# ๐์คํ๋ง์ผ
>ํ๋ก์ ํธ ๊ธฐ๊ฐ 2022.05.09 ~ 2022.06.03
>
>ํ์ : [marisol](https://github.com/marisol-develop), [Eddy](https://github.com/kimkyunghun3) / ๋ฆฌ๋ทฐ์ด : [๋ฆฐ์](https://github.com/jungseungyeo)
## ๋ชฉ์ฐจ
- [ํ๋ก์ ํธ ์๊ฐ](#ํ๋ก์ ํธ-์๊ฐ)
- [ํค์๋](#ํค์๋)
- [๊ณ ๋ฏผํ์ ](#๊ณ ๋ฏผํ์ )
- [์คํ๋ง์ผI](#์คํ๋ง์ผI)
- [์คํ๋ง์ผII](#์คํ๋ง์ผII)
## ํ๋ก์ ํธ ์๊ฐ
์คํ๋ง์ผ ๋ง๋ค๊ธฐ!
## ์คํ ํ๋ฉด
์ฑ์ ์ต์ด์ ์คํํ์ ๋ ๋ฐ์ดํฐ๊ฐ ๋ก๋๋ ๋๊น์ง indicator๋ฅผ ๋ณด์ฌ์ค๋ค.

-----
๋์ผํ ๋ฐ์ดํฐ๋ฅผ List, Grid ํ์์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ๋ณด์ฌ์ค๋ค.

-----
์คํฌ๋กค์ ๋ด๋ฆฌ๊ฒ ๋๋ฉด ๋คํธ์ํฌ ํต์ ์ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ๋ค.

-----
์คํฌ๋กค์ ์๋ก ์ฌ๋ฆฌ๊ฒ ๋๋ฉด refresh๋ก ํต์ ์ ํด์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ๋ค.

-----
์ํ์ ๋ฑ๋กํ๋ฉด POST ํธ์ถ์ ํตํด ์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ๋ฑ๋กํ๋ค.

-----
์ํ ์์ ํ๋ฉด PATCH ํธ์ถ์ ํตํด ์๋ฒ์ ์์ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฑ๋กํ๋ค.

-----
์ํ ์ญ์ ํ๋ฉด DELETE ํธ์ถ์ ํตํด ์๋ฒ์์๋ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ค.

## ๊ฐ๋ฐํ๊ฒฝ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ



## ํค์๋
`git flow` `Test Double` `URLSession` `StubURLSession` `Protocol Oriented Programming` `์ถ์ํ` `json` `HTTP method` `decode` `escaping closure` `Cache` `CompositionalLayout` `URLSession` `UIImagePickerController`
### ์์ธํ ๊ณ ๋ฏผ ๋ณด๊ธฐ
#### [STEP1](https://github.com/yagom-academy/ios-open-market/pull/142)
#### [STEP2](https://github.com/yagom-academy/ios-open-market/pull/153)
#### [STEP3](https://github.com/yagom-academy/ios-open-market/pull/164)
#### [STEP4](https://github.com/yagom-academy/ios-open-market/pull/170)
# ๐์คํ๋ง์ผI
## ๐ง ๊ณ ๋ฏผํ์ & ํด๊ฒฐ๋ฐฉ๋ฒ
### ๐ 1. URLSessionDataTask์ init์ด deprecated๋ ๋ฌธ์ ๋ฅผ ์์กด์ฑ ์ฃผ์
์ผ๋ก ํด๊ฒฐ
> Response๋ผ๋ ๊ตฌ์กฐ์ฒด๋ฅผ ํ์ฉํด์ ์์กด์ฑ ์ฃผ์
ํ์ฌ URLSessionDataTask๋ฅผ ์์๋ฐ๋ ๊ฒ ์๋๋ผ ๋ง๋ค์ด์ง ํ๋กํ ์ฝ ์ฃผ์
์ ํตํด ํด๊ฒฐํ๋ค.
```swift
import Foundation
typealias DataTaskCompletionHandler = (Response) -> Void
struct Response {
var data: Data?
var statusCode: Int
var error: Error?
}
protocol URLSessionProtocol {
func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskCompletionHandler)
func dataTask(with url: URL, completionHandler: @escaping DataTaskCompletionHandler)
}
extension URLSession: URLSessionProtocol {
func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskCompletionHandler) {
dataTask(with: request) { data, response, error in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else {
return
}
let response = Response(data: data, statusCode: statusCode, error: error)
completionHandler(response)
}.resume()
}
func dataTask(with url: URL, completionHandler: @escaping DataTaskCompletionHandler) {
dataTask(with: url) { data, response, error in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else {
return
}
let response = Response(data: data, statusCode: statusCode, error: error)
completionHandler(response)
}.resume()
}
}
```
### ๐ 2. MockURLSession Unit Test
๊ฐ์ง ๋ฐ์ดํฐ๋ฅผ ํ์ฉํด์ ๋คํธ์ํฌ ํต์ ์ด ์ ๋๋์ง ํ์ธ
```swift
func test_Product_dummy๋ฐ์ดํฐ์_totalCount๊ฐ_10๊ณผ_์ผ์นํ๋ค() {
// given
let promise = expectation(description: "success")
var totalCount: Int = 0
let expectedResult: Int = 10
guard let product = NSDataAsset(name: "products") else {
XCTFail("Data ํฌ๋งทํ
์คํจ")
return
}
let response = Response(data: product.data, statusCode: 200, error: nil)
let dummyData = DummyData(response: response)
let stubUrlSession = StubURLSession(dummy: dummyData)
sutProduct.session = stubUrlSession
// when
sutProduct.execute(with: FakeAPI()) { result in
switch result {
case .success(let product):
totalCount = product.totalCount
case .failure(let error):
XCTFail(error.localizedDescription)
}
promise.fulfill()
}
wait(for: [promise], timeout: 10)
// then
XCTAssertEqual(totalCount, expectedResult)
}
```
### ๐ 3. list cell๊ณผ grid cell์ dataSource ํ์ฉ
์ฒ์์ MainViewController์ makeDataSource ๋ฉ์๋ ๋ด๋ถ์์ ๋ฐ์ดํฐ๋ค์ ๊ฐ์ง๊ณ cell์ ๊ตฌ์ฑํด์ฃผ์๋๋ฐ, ๊ทธ๋ ๊ฒ ํ๋ค๋ณด๋ makeDataSource ๋ฉ์๋๊ฐ ๋๋ฌด ๊ธธ์ด์ง๊ณ ์ญํ ๋ถ๋ฆฌ๋ ์ ๋งคํด์ก๋ค๋ ์๊ฐ์ด ๋ค์๋ค. ๊ทธ๋์ configureCell์ด๋ผ๋ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ก product๋ฅผ ๋ฐ๋๋ก ํ๊ณ , cell ๋ด๋ถ์์ ํ๋ผ๋ฏธํฐ์ product ๋ฐ์ดํฐ๋ก cell์ ๊ตฌ์ฑํด์ฃผ๋๋ก ๋ณ๊ฒฝํ๋ค.
> ๊ฐ๊ฐ grid, list cell์ ๋ฐ๋ผ View์ ์ง์ ์ ์ผ๋ก ๋ฟ๋ ค์ฃผ๊ฒ ๋๋ฉด View์ ๋ก์ง์ด ๋ ํ์ํ๊ธฐ ๋๋ฌธ์ Presenter๋ผ๋ ์ค๊ฐ ๋งค์ฒด๋ฅผ ์ฌ์ฉํ๋ค. ์ด ๊ณณ์์๋ cell์์ ํต์ ์ ํตํด ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ ํ์ฉํด์ ๋ทฐ์ ํ์ํ ๋ก์ง๋ค์ ๊ตฌํํ๋ค.
์๋ฅผ ๋ค์ด, ๋ฐ์ดํฐํฌ๋งทํฐ๋ฅผ ํตํด ์ผํ๋ฅผ ๋ฃ์ด์ฃผ๊ฑฐ๋ ์ฌ๊ณ ์๋์ ๋ฌด์ ๋ฐ๋ผ cell์ ๋ค๋ฅด๊ฒ ๋ณด์ฌ์ค์ผํ๋ ๋ถ๋ถ์ ์ฒ๋ฆฌํ์ฌ์ View์์๋ ๋ณด์ฌ์ฃผ๋ ์ญํ ๋ง์ ์ํํ๋๋ก ๋ถ๋ฆฌํ๋ค.
### ๐ 4. collectionView์ cell์ ๊ตฌ๋ถ์ ์ ๊ทธ๋ฆฌ๊ธฐ
cell๋ง๋ค ๊ตฌ๋ถ์ ์ ์ฃผ๋ ๋ฐฉ๋ฒ์ ๋ํด ์ค๋ ๊ณ ๋ฏผํ๋ค ๋ค๋ฅธ ์บ ํผ๋ค์๊ฒ ์กฐ์ธ์ ๊ตฌํด view๋ฅผ ์๊ฒ ๋ง๋ค์ด์ ๋ฃ๋ ๋ฐฉ๋ฒ์ด ์๋ค๋ ๊ฒ๋ ์๊ฒ ๋์๊ณ , CALayer๋ฅผ extensionํด์ bottom์ ๊ตฌ๋ถ์ ์ ๋ฃ์ด์ฃผ๋ ๋ฐฉ๋ฒ๋ ์๊ฒ ๋์ด, CALayer๋ฅผ extensionํ๋ ๋ฐฉ๋ฒ์ ์ฑํํ๋ค.
> cell ์์ view๋ฅผ ๋ฃ์ด์ ๊ตฌ๋ถ์ ์ ๋ฃ๋ ๋ฐฉ๋ฒ์ด ์กด์ฌํ๋ค๋ ๊ฒ์ ์๊ฒ ๋์๋ค. ํ์ง๋ง ์๋ ์ฝ๋๋ก CALayer๋ฅผ ํ์ฅํ์ฌ ์ฌ์ฉํ๋ฉด ๋ ํธ๋ฆฌํ๋ค๊ณ ํ๋จํ์ฌ ์๋์ ์ฝ๋๋ฅผ ์ฌ์ฉํ๋ค.
```swift=
extension CALayer {
func addSeparator() {
let separator = CALayer()
separator.frame = CGRect.init(x: 10, y: frame.height - 0.5, width: frame.width, height: 0.5)
separator.backgroundColor = UIColor.systemGray2.cgColor
self.addSublayer(separator)
}
}
```
### ๐ 5. modern CollectionView์ ๋ทฐ์ ์ ํ์ ๋น๋๊ธฐ๋ก ๊ตฌํ
segmentedControl์ index๋ฅผ ํ์ฉํด์ ๋ทฐ์ ์ ํ์ ๊ตฌํํ์๋ค.
ํ์ง๋ง ์ด ์ฝ๋๋ modern ColletctionView์์ ์ ๊ณตํ๋ init์ ๋น๋๊ธฐ์ ์ผ๋ก ๋ทฐ์ ์ ํ์ ํ์ฉํ์ง ์๋ ์ฝ๋์๋ค.
๊ทธ๋ฆฌํ์ฌ ๋ด๋ถ์ ์กด์ฌํ๋ init๋ฅผ ํ์ฉํด์ ์ฒ๋ฆฌํด๋ณด์๋ค.
```swift
public init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider,
configuration: UICollectionViewCompositionalLayoutConfiguration)
```
์ด init๋ ๋ทฐ ์์ ์ฌ๋ฌ Section๋ค์ ๋ํ ์ ํ์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํด์ฃผ๋ฏ๋ก ์ด์ ์ฝ๋์ ๋ค๋ฅด๊ฒ ๋ ํจ์จ์ ์ผ๋ก ๊ฐ๋ฅํด์ง๋ค.
์๋์ฒ๋ผ ๊ตฌํํ์ผ๋ฉฐ
```swift
private lazy var layouts: UICollectionViewCompositionalLayout = {
return UICollectionViewCompositionalLayout(sectionProvider: { _, _ in
return LayoutType.section(self.layoutType)()
}, configuration: .init())
}()
```
์ด๊ณณ์์ layoutType๋ enum๋ฅผ ํ์ฉํด์ ๋ด๋ถ๋ฅผ ์บก์ํํด์ ์ฌ์ฉํ๋ค. ๊ทธ๋ฌ๋ฉด ์ธ๋ถ์์์ ์ ๊ทผ๋ ํผํ ์ ์์ผ๋ฉฐ segmentedIndex๋ฅผ ํ์ฉํ์ง ์๊ณ ๋ ๋ทฐ์ ๋ณํ์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์๋ค.
### ๐ 6. ์ด๋ฏธ์ง ์บ์ฑ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ
NSCache๋ฅผ ์ฌ์ฉํด์ ์ด๋ฏธ์ง๋ฅผ ์บ์ฑํ๋๋ก ํ๋ค.
UIImageView๋ฅผ extensionํด์ `loadImage(_ urlString: String)`๋ผ๋ ๋ฉ์๋๋ฅผ ๋ง๋ค๊ณ ,
์ด๋ฏธ ์บ์์ ํด๋น url์ ์ด๋ฏธ์ง๊ฐ ์กด์ฌํ๋ค๋ฉด ํด๋น ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ๋๋ก ํ๊ณ , ์บ์์ ์๋ ์ด๋ฏธ์ง๋ผ๋ฉด ํด๋น url๋ก ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค์ด ์บ์์ ์ ์ฅํ๋๋ก ํ๋ค.
### ๐ 7. ๋ทฐ์ ancestor ๋ฌธ์
๋ทฐ์ ๋ ์ด์์์ ์ก๊ณ ๋น๋๋ฅผ ํ์ ๋ ์๋์ ๊ฐ์ ๋ฌธ์ ๊ฐ ์์ฃผ ๋ฐ์ํ๋ค.
> Thread 1: "Unable to activate constraint with anchors <NSLayoutXAxisAnchor:0x6000034dcec0 \"UILabel:0x12260c090.centerX\"> and <NSLayoutXAxisAnchor:0x6000034dcf00 \"UIView:0x12260ab30.centerX\"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal."
์ด ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ ์ 2๊ฐ์ง๋ฅผ ๊ณ ๋ คํ๋ฉด ๋๋ค.
1. addSubview๊ฐ ์๋์ด์๋์ง
2. addSubview -> Layout Constraint ์์ผ๋ก ์ํ๋์ง
### ๐ 8. cell์ indexPath ๋น๊ต vs cell prepareForReuse
#### prepareForReuse
- ์
์ ์ฌ์ฌ์ฉํ๋ ๋ฉ์๋๋ค
- CollectionView์ ์ฅ์ ์ ์
์ ์ฌ์ฌ์ฉํ๋ค๋๊ฒ. ๊ทธ๋์ ์ด ๋ฉ์๋๊ฐ ํจ์จ์ ์ผ๋ก ๋ฐํํ ์ ์๋ค.
- ๋ํ Object ์ฑ
์์ ๋งํ๊ธธ, "๊ฐ์ฒด์งํฅ์์ ๊ฐ์ฒด ์ฌ์ด์์ ์๋ก์ ์ ๋ณด๋ฅผ ๋ง์ด ์์๋ก ์์กด ๊ด๊ณ๊ฐ ๋์์ง๋ค."๊ณ ํ๋๋ฐ, indexPath๊ฐ cell์ ์ ๋ณด๋ฅผ ์๋ ๊ฒ์ด ๊ฐ์ฒด ์ฌ์ด์ ์ ๋ณด๋ฅผ ๊ณต์ ํ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์ด๋ ์์กด๊ด๊ณ๋ฅผ ๋์ผ ์ ์๋ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ํ๋ค.
- ๊ฐ์ ๋ถ๋ถ์ ์ผ๋ก updateํ ์ ์๋ ์ฅ์ ์ด ์กด์ฌํ๋ค.
#### indexPath
- indexPath๋ฅผ ์ฌ์ฉํ๋ฉด ๊ณ ์ ์ ์ผ๋ก update๋ฅผ ํด์ค ์ ์๋ค
- ์ฌ๊ธฐ์์๋ ์ฌ์ฌ์ฉ ์
์ ์ฌ์ฉํ์ง ์๊ณ ๊ณ ์ ์ ์ผ๋ก ์ ์ ํ ๊ณณ์ ์
๋ฐ์ดํธ ํด์ฃผ๋ฏ๋ก ๊ณ ์ ์ ์ผ๋ก ์ฌ์ฉํ๋ค๋ฉด ์ ๋ฆฌํ ๋ฏ์ถ๋ค.
- ํ์ง๋ง ์ฌ์ฌ์ฉ์ฑ์ด ๋จ์ด์ง๋ฏ๋ก ์ด๋ฅผ ์ฃผ์ํด์ผํ๋ฉฐ, ๊ด๋ฆฌ๋ฅผ ํด์ค ์ ์๋ ๋จ์ ์ด ์กด์ฌํ๋ค.
> ์์ ๊ฐ์ ํน์ฑ์ด ์๊ณ , collectionView์ ์ฅ์ ์ธ ์ฌ์ฌ์ฉ์ฑ์ ์ด์ฉํ prepareForReuse๋ฅผ ์ฌ์ฉํ๋๋ก ๋ณ๊ฒฝํ์๋ค
---
# ๐์คํ๋ง์ผII
## ๐ง ๊ณ ๋ฏผํ์ & ํด๊ฒฐ๋ฐฉ๋ฒ
### ๐ 1. ๊ณตํต ์ฝ๋์ ํ๋กํ ์ฝ ๊ธฐ๋ณธ ๊ตฌํ ํ์ฉ
์ํ ๋ฑ๋กํ๋ฉด, ์ํ ์์ ํ๋ฉด View ํ๋กํ ์ฝ ์ถ์ํ๋ก ์ธํ ์๋ํ ๋ถ๊ฐ๋ฅ ๋ฌธ์
> ํ๋กํ ์ฝ ์ถ์ํ์ ๋ฐ๋ฅธ ์๋ํ ๋ถ๊ฐ๋ฅํ์ง๋ง ๋
ธ์ถ๋๋ฉด ์๋๋ ๋ถ๋ถ์ ๋ํด์๋ ์ถ์ํ๋ฅผ ํ๋ฉด ์๋๊ณ ์ฌ์ฌ์ฉ์ฑ ์์ผ๋ฉฐ ์๋ํ๊ฐ ํ์ํ์ง ์๋ ๋ถ๋ถ์ ์ฌ์ฉํ๋ฉด ๋๋ค.
```swift
protocol Drawable: UIView {
var entireStackView: UIStackView { get }
var productInfoStackView: UIStackView { get }
var priceStackView: UIStackView { get }
var priceTextField: UITextField { get }
var segmentedControl: UISegmentedControl { get }
var productNameTextField: UITextField { get }
var discountedPriceTextField: UITextField { get }
var stockTextField: UITextField { get }
var descriptionTextView: UITextView { get }
func configureView()
func configurePriceStackView()
func configureProductInfoStackView()
func configureEntireStackViewLayout()
func makeStackView(axis: NSLayoutConstraint.Axis, alignment: UIStackView.Alignment, distribution: UIStackView.Distribution, spacing: CGFloat) -> UIStackView
}
```
### ๐ 2. ๋ค๋ฅธ ๋ทฐ์ ํ๋กํผํฐ ์ ๋ฌํ๋ ๋ฐฉ๋ฒ
๋ค๋ฅธ ๋ทฐ์ ํ๋กํผํฐ ์ ๋ฌ ์ ๋์ผํ ์ํ์ ์๋ ค์ฃผ๊ธฐ ์ํด id๋ฅผ ํ์ฉํ๋ ์์ ์์์ id๊ฐ์ ์๋ํ ๋ฌธ์
> ProductDetailViewController์ ProductEditViewController๊ฐ ์ด๊ธฐํ ๋ ๋ id์ productDetail์ ๋ฐ๋๋ก ๊ตฌํํ์ฌ ์๋ํ ๋ฌธ์ ํด๊ฒฐ
```swift
// ProductEditViewController.swift
private let products: Products
init(productDetail: ProductDetail) {
self.productDetail = productDetail
super.init(nibName: nil, bundle: nil)
}
// MainViewController.swift
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let productDetailViewController = ProductDetailViewController(products: item[indexPath.row])
self.navigationController?.pushViewController(productDetailViewController, animated: true)
}
}
```
### ๐ 3. UIImagePicker์์ ์ ํํ ์ด๋ฏธ์ง๋ฅผ ์์๋๋ก ๋ณด์ฌ์ฃผ๋ ๋ฐฉ๋ฒ
image๊ฐ ์ ํ๋์์๋, `didFinishPickingMediaWithInfo` ๋ฉ์๋ ๋ด์์
์ ํ๋ ์ด๋ฏธ์ง๋ฅผ `imageArray์` appendํ๋ค.
๊ทธ๋ฆฌ๊ณ `cellForItemAt` ๋ฉ์๋์์ `imageArray[indexPath.row]`๋ฒ์งธ ์ด๋ฏธ์ง๋ฅผ `button.setImage` ํด์ฃผ๋ ๋ฐฉ์์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ์์๋๋ก ๋ณด์ฌ์ฃผ๋๋ก ๊ตฌํํ๋ค.
### ๐ 4. ์ฌ๋ฌ ๊ฐ์ ImageView ๋ณด์ฌ์ง๋๋ก ํ๋ ๋ฐฉ๋ฒ
์ด๋ฏธ์ง ๋ทฐ๋ฅผ ์ฌ๋ฌ๊ฐ ๋ฑ๋ก ์, ๊ฐ๊ฐ ๋ค๋ฅธ ์ด๋ฏธ์ง๋ทฐ๊ฐ ๋์ค์ง ์๋ ์๋ฌ
> cellForItemAt ๋ฉ์๋๊ฐ ๋ถ๋ฆด ๋๋ง๋ค cell์ imageView๋ฅผ addSubviewํด์ฃผ๊ณ ,
presenter์ images ๋ฐฐ์ด ์ค indexPath.row ๋ฒ์งธ์ ์ด๋ฏธ์ง๋ฅผ ํด๋น imageView์ image์ ๋ฃ์ด์ฃผ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐํ๋ค.
```swift
func setImage(_ presenter: Presenter) {
guard let productImages = presenter.images else {
return
}
for image in productImages {
guard let url = URL(string: image) else {
return
}
guard let data = try? Data(contentsOf: url) else {
return
}
imageView.image = UIImage(data: data)
contentView.addSubview(imageView)
}
}
```
```swift
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: EditViewCell.identifier, for: indexPath) as? EditViewCell else {
return UICollectionViewCell()
}
cell.contentView.addSubview(cell.imageView)
guard let imageArray = presenter.images else {
return UICollectionViewCell()
}
guard let imageString = imageArray[indexPath.row].url,
let imageURL = URL(string: imageString),
let imageData = try? Data(contentsOf: imageURL) else {
return UICollectionViewCell()
}
let image = UIImage(data: imageData)
cell.imageView.image = image
return cell
}
```
### ๐ 5. ImageView ์ฉ๋ ์ฌ์ด์ฆ ์ฒดํฌ
์ด๋ฏธ์ง ์ฉ๋์ ์ฒดํฌํ ํ resizeํ์ฌ ์ ์ฅํ๋๋ก ํ๋ค.
๊ทธ๋ฌ๋ ์ค์ ๋ฆฌ์ฌ์ด์ฆํ ๊ฒ์ ๋ณด๊ฒ ๋๋ฉด ์๊ตฌ์ฌํญ ๋ณด๋ค ๋ ์์ ์ด๋ฏธ์ง ํฌ๊ธฐ๊ฐ ๋๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค. ๊ทธ๋์ ์ด๋ฅผ ๋ ์ด์์์ผ๋ก ํค์์ ๊ฝ์ฐจ๋๋ก ํด์ผ ํ๋ ๊ฒ์ธ์ง ์๋๋ฉด ์ด๋๋ก ์์์ง ํฌ๊ธฐ๊ฐ ์ ์ ํ ๋ฐฉ๋ฒ์ด์๋์ง ๊ณ ๋ฏผ๋์๋ค.
```swift
// ๊ตฌํ๋ถ
extension UIImage {
func resize(newWidth: CGFloat) -> UIImage {
let scale = newWidth / self.size.width
let newHeight = self.size.height * scale
let size = CGSize(width: newWidth, height: newHeight)
let renderer = UIGraphicsImageRenderer(size: size)
let renderedImage = renderer.image { context in
self.draw(in: CGRect(origin: .zero, size: size))
}
return renderedImage
}
func checkImageCapacity() -> Double {
var capacity: Double = 0.0
guard let data = self.pngData() else {
return 0.0
}
capacity = Double(data.count) / 1024
return capacity
}
}
```
```swift
// ํธ์ถ๋ถ
guard let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage else {
return
}
let imageCapacity = image.checkImageCapacity()
if imageCapacity > 300 {
let resizedImage = image.resize(newWidth: 80)
self.imageArray.append(resizedImage)
guard let imageData = resizedImage.jpegData(compressionQuality: 1) else {
return
}
let imageInfo = ImageInfo(fileName: "marisol.jpeg", data: imageData, type: "jpeg")
self.networkImageArray.append(imageInfo)
} else {
self.imageArray.append(image)
guard let imageData = image.jpegData(compressionQuality: 1) else {
return
}
let imageInfo = ImageInfo(fileName: "marisol.jpeg", data: imageData, type: "jpeg")
self.networkImageArray.append(imageInfo)
}
```
> RegistrationViewCell์์ imageView์ frame์ cell์ width์ height๋ก ์ค์ ํด์ฃผ์ด ์ด๋ฏธ์ง๊ฐ cell์ ๊ฝ ์ฐจ๊ฒ ๋์ค๋๋ก ํ๋ค.
```swift
imageView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width)
```
### ๐ 6. ์ํ secret ์กฐํ๋ฅผ ์ํ secretPost ๊ตฌํ
DELETE ํ๊ธฐ ์ secretPOST๋ฅผ ํธ์ถ ์์ ๋ฌธ์ , ๋ ๋ค๋ฅธ httpMethod ์ผ์ด์ค๋ก ๋ถ๋ฆฌ ๊ฒฐ์ ๋ฌธ์
> httpMethod๋ GET/POST/PATCH/DELETE ์ด๋ ๊ฒ 4๊ฐ๋ง ์์ด์ผ ํ๋ค๊ณ ์๊ฐํ๊ณ ,
POST ๋ด๋ถ์์ ๋ถ๊ธฐ ์ฒ๋ฆฌํ์ฌ ๊ตฌํํ๋ค.
์ค์ ํธ์ถ๋ถ์์๋ CompletionHandler๋ฅผ ํ์ฉํด์ secretPOST๊ฐ ์คํ๋๊ณ ์ฑ๊ณตํ ๊ฐ์ ์ด์ฉํ์ฌ DELETE์ ํ์ฉํด์ ๊ตฌํํ๋ค.
```swift
mutating private func makePOSTRequest(apiAble: APIable) -> URLRequest? {
let boundary = generateBoundary()
if let item = apiAble.item {
let urlString = apiAble.url + apiAble.path
guard let url = URL(string: urlString) else {
return nil
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("multipart/form-data; boundary=\"\(boundary)\"",
forHTTPHeaderField: "Content-Type")
request.addValue(UserInformation.identifier, forHTTPHeaderField: "identifier")
request.addValue("eddy123", forHTTPHeaderField: "accessId")
request.httpBody = createPOSTBody(requestInfo: item, boundary: boundary)
return request
} else {
return makeSecretPOSTRequest(apiAble: apiAble)
}
```
### ๐ 7. View์ ๋ก์ง์ VC์์ ์ฌ์ฉํจ์ ๋ฐ๋ฅธ ์๋ํ ๋ฌธ์
View์์ ๋ก์ง ๊ตฌํ์ ํ๋ ๊ฒ์ด ์ณ์ง ์๊ธฐ์ VC์์ ๋ก์ง์ ๊ตฌํํ๋ ค๊ณ ๋
ธ๋ ฅํ๋ค. ๊ทธ๋ ๊ฒ ํ๋ค๋ณด๋ View์์ ๋ง๋ Label๋ฅผ ์ ๊ทผํด์ ํด์ผํ๊ธฐ ๋๋ฌธ์ ์๋ํ๋ฅผ ํ ์ ์๋ ์ํฉ์ด ์์๋๋ฐ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์๋ํ๋ฅผ ์๋ํด์ผํ ์ง ๊ณ ๋ฏผํ๋ค.
```swift
final class ProductDetailView: UIView {
...
private let entireScrollView = UIScrollView()
private let productImage = UIImageView()
let stockLabel = UILabel()
let priceLabel = UILabel()
let discountedLabel = UILabel()
let productNameLabel = UILabel()
private let descriptionTextView = UITextView()
private let currencyLabel = UILabel()
let pageControl = UIPageControl()
}
// VC ์ฌ์ฉ์ฒ
private func configureProductNameUI() {
productDetailView.productNameLabel.font = UIFont.boldSystemFont(ofSize: 15)
}
private func configureStockUI() {
productDetailView.stockLabel.textColor = .systemGray2
}
``ํฌ
> ์ด๋ฐ ๊ฒฝ์ฐ view์ controller๊ฐ ๋ชจ๋ ํด๋น ๊ฐ์ ์ ๊ทผํด์ผ ํ๊ธฐ ๋๋ฌธ์, view๋จ์์ ๊ทธ๋๋ก ๊ฐ์ ๋
ธ์ถํ๋๋ก ๋์๋ค.
### ๐ 8. ๋คํธ์ํฌ ์ถ์ํ
์ฐ์ ๊ฐ ๊ฐ์ฒด์ ์ญํ ์ ์๋์ ๊ฐ์ด ์๊ฐํ๋ค.
- `ViewController`
- ์ ๋ณด (ex: pageNo, itemsPerPage, productId)๋ฅผ ๊ฐ์ง๊ณ APIable์ request ๋ฉ์๋ ํธ์ถ
- `APIable`
- VC์๊ฒ ๋ฐ์ ์ ๋ณด๋ก request ๋ฉ์๋์์ get / post / patch / delete ๊ตฌ๋ถํด์ NetworkManager์ ๋ฉ์๋ (execute) ํธ์ถ
- `NetworkManager`
- APIable์๊ฒ ์ ๋ฌ๋ฐ์ host, path, params ๋ฑ ์กฐํฉํด์ url ์์ฑ
- url๋ก request ๋ง๋ค์ด์ ๋คํธ์ํฌ ํต์
- ํต์ ํ ๊ฒฐ๊ณผ๋ฅผ VC์ ์ ๋ฌ
APIable์ ๋จผ์ ๊ตฌํํ๋๋ฐ,
VC๋ APIable์ request ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ GET์ธ์ง, POST์ธ์ง๋ฅผ ์๊ณ ์์ ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์, request ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ๊ฐ์ ๋ฐ๋ผ request ๋ฉ์๋ ๋ด๋ถ์์ httpMethod๋ฅผ ์ ํด์ค์ผ ํ๋ค๊ณ ์๊ฐํ๋ค.
๊ทธ๋ฐ๋ฐ ํ๋ผ๋ฏธํฐ์ ํ์์ด ๋ค์ํ๋ค๋ณด๋, APIable์ ๋ณ์๋ค๊ณผ request์ ํ๋ผ๋ฏธํฐ ์ข
๋ฅ๊ฐ ๋ง์์ก๋ค..
```swift
protocol APIable {
var method: HTTPMethod? { get }
var baseUrl: String? { get }
var listParams: [String: String]? { get }
var registrationParams: String? { get }
var updateParams: Int? { get }
var deleteParams: String? { get }
var secretParams: String? { get }
var images: [ImageInfo]? { get }
func request(_ list: [String: String]?, _ registration: String?, _ update: Int?, _ delete: String?, _ secret: String?, completionHandler: @escaping (Result<Any, Error>) -> Void)
}
```
> APIable์๋ url์ ๋ง๋ค๊ธฐ ์ํ base url, path, params์ httpmethod๋ง ๊ฐ์ง๊ณ ์๋๋ก ๊ตฌํํ๊ณ ,
๋ฑ๋ก, ์ํ ์์ธ ์กฐํ, ์์ , ์ญ์ ๋ฑ๊ณผ ๊ฐ์ ์ํฉ๋ง๋ค APIable์ ์ฑํํ๋ ๊ตฌ์กฐ์ฒด๋ฅผ ๋ง๋ค์ด์คฌ๋ค.
```swift
protocol APIable {
var url: String { get }
var path: String { get }
var method: HTTPMethod { get }
var item: Item? { get }
var params: [String: String]? { get }
var productId: Int? { get }
var secret: String? { get }
}
struct List: APIable {
let url: String = "https://market-training.yagom-academy.kr/"
let path: String = "api/products"
let method: HTTPMethod = .get
let item: Item? = nil
let pageNo: Int
let itemsPerPage: Int
var params: [String: String]? {
return ["page_no": String(pageNo),
"items_per_page": String(itemsPerPage)]
}
var productId: Int? = nil
var secret: String? = nil
```