# My Market ๐ช (MVC)
> ํ๋ก์ ํธ ๊ธฐ๊ฐ: 2022-11-10 ~ 2022-12-02
## ํ์
[์ก๊ธฐ์](https://github.com/kiwi1023), [์ ํ์](https://github.com/yusw10), [์ด์์ฐฌ](https://github.com/apwierk2451)
## ๐ ํ๋ก์ ํธ ์๊ฐ
**์๋ฒ์์ ๋คํธ์ํน์ ํตํด ์ํ์ ๋ฑ๋ก, ์์ , ์ญ์ ๊ฐ ๊ฐ๋ฅํ ๋๋ง์ ๋ง์ผ**
## ๐ ํค์๋
- `UIKit`
- `Network`
- `URLSession Mock Test`
- `Json Decoding Strategy`
- `NSCache`
- `XCTestExpection`
- `completionHandler`
- `Escaping Closure`
- `URLSession`
- `RefreshController`
- `Test Double`
- `UICollectionView`
- `DiffableDataSource`
- `CompositionalLayout`
## ๐ฑ ํ๋ก์ ํธ ์คํํ๋ฉด
|๋ฉ์ธํ๋ฉด (๋ฐฐ๋๋ทฐ)|๋ฌดํ์คํฌ๋กค|UISearch Bar ๊ตฌํ
|-|-|-|
||
|์ํ ๋ฑ๋ก|์ํ ์์ |์ํ ์ญ์
||
## ๐ํธ๋ฌ๋ธ ์ํ
###
<details>
<summary>Launch Screen ์ด์</summary>
์ด๊ธฐ CollectionView๋ฅผ ์ค์ ํ๋ฉด์ ํ๋ฉด์ ํ์ธํด๋ณด์๋๋ฐ ๋ค์๊ณผ ๊ฐ์ด ์ํ๋จ์ ์์ญ์ด ์๋ ค์ ๋์ค๋๊ฑธ ํ์ธํ ์ ์์๋ค.
<img src=https://i.imgur.com/xdtPbDP.png width=40%>
CollectionView์ ๊ฐ anchor๋ฅผ ๋ฉ์ธ View Controller์ View์ safeAreaLayoutGuide์ ๋ง์ถฐ์ฃผ์ง ์์๋ค๊ณ ์๊ฐ๋์ด ๋ทฐ ๊ณ์ธต ์ฐฝ์ ๋ณด์๋๋ฐ ์คํ๋ ค ๋ทฐ ๊ณ์ธต์์์๋ ์ ํ ๋ฌธ์ ๊ฐ ์์๋ค.
<img src=https://i.imgur.com/vhbb1HH.png width=40%>
์๋ ธ๋ค๊ธฐ ๋ณด๋ค๋ ์์ window์์ฒด๊ฐ ์๊ฒ ์กํ์๋ค๋ ๊ฒ์ ๊ฐ๊น์ด ํํ์๋ค.
<img src=https://i.imgur.com/9z4bjck.png width=50%>
๋ฌธ์ ๋ ์์ ํ์ง ๋ชปํ๊ณณ์์ ๋ฐ์ํ๊ณ ์์๋ค.
ํ๋ก์ ํธ์ UI๋ฅผ ์ฝ๋๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝํ๋ฉด์ ๊ธฐ๋ณธ์ผ๋ก ์์ฑ๋๋ ์คํ ๋ฆฌ๋ณด๋ ํ์ผ๋ค์ ๋ชจ๋ ์ ๊ฑฐํ๋ ๊ณผ์ ์ ๊ฑฐ์ณค๋ค.
๊ทธ ๊ณผ์ ์ค์ LaunchScreen์ ์์ฑํ๋ ์ต์
์ ๊ป๋๋ฐ ์ด ์ต์
์ ๊บผ๋ฒ๋ฆฌ๋ ์์ ๊ฐ์ด window ์์ฒด๊ฐ ์๊ฒ ์กํ๋ค.
์ด ์ต์
์ ๋ค์ ํด์ผ๋ก์จ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์์ง๋ง, ์ ํํ ์ด๋ค ์๋ฆฌ๋ก ์ด์๊ฐ์ด ๋์๋์๋์ง ๊ด๋ จ ๊ธ์ด ๋ถ์กฑํด์ ์ ์ ์์๋ค...(๋ฐ์น ์คํฌ๋ฆฐ์ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ๊ธ์ด ์ฃผ๋ฅ์๋ค)
๋ค๋ง ์์ํด๋ณด์๋ฉด, SceneDelegate์์ ์๋์ฐ๋ฅผ ์ธ์คํด์คํ ํ๋ ๊ณผ์ ์์ ๊ธฐ์กด์๋ ๋ฐ์น์คํฌ๋ฆฐ์ด ํ๋ฉด ์ ์ฒด ํฌ๊ธฐ์ ๋ง๊ฒ ์ต์์ Frame์ ์ก๊ณ ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก windowํฌ๊ธฐ๊ฐ ์กํ์์ง๋ง, ์ฐ๋ฆฌ์ ์ฝ๋์์๋ ์ด ๊ณผ์ ์ด ์๋ต๋์ด์ ์ปจํ
์ธ ์ต์ ํฌ๊ธฐ๋๋ก ์๋์ฐ๊ฐ ์ค์ ๋๊ฒ์ผ๋ก ์ถ์ธกํ๋ค. ์๋ง๋ SceneDelegate์์ Scene์ ์์ฑ์์ ์๋์ฐ ํฌ๊ธฐ๋ฅผ ์คํฌ๋ฆฐ ํฌ๊ธฐ๋ก ์ง์ ํด์ค๋ค๋ฉด ์๋์ฐ ํฌ๊ธฐ๊ฐ ์๋ํ ๋๋ก ๋์ค์ง ์์๊น ์ถ๋ค.
</details>
###
<details>
<summary> ์ํ ๋ชฉ๋ก ํ๋ฉด์ด๋์ ์์น๋ฐ๊ฐ ๋ณด์ฌ์ง๋๋ก ์์ </summary>
์ํ ๋ชฉ๋ก ํ๋ฉด์ผ๋ก ์ง์
์ ๋ค์๊ณผ ๊ฐ์ด ์์น๋ฐ๊ฐ ๋ณด์ฌ์ง๋๋ก ์ค์ ํ๊ณ ์ ํ๋ค.
<img src=https://i.imgur.com/S5HlxtC.png width=50%>
์ด๋ฅผ NavigationItem์ `hidesSearchBarWhenScrolling` ์์ฑ์ ํตํด ์ง์ ํ๊ณ ์ ํ๋๋ฐ ๋ทฐ ์ง์
์ ์์น๋ฐ๊ฐ ๋ณด์ด๊ฒ ํ๊ธฐ ์ํด ์ด ์์ฑ์ false๋ก ํ๋ฉด ์คํฌ๋กค์ ์์ฐ์ค๋ฝ๊ฒ ์ฌ๋ผ์ง์ง ์์๋ค.
๋ฐ๋ผ์ ๋ทฐ ์ต์ด ์ง์
ํ์ฌ ViewWillAppear์์ ์ด๋ฅผ ํด์ ํ์ฌ ์์น๋ฐ๊ฐ ๋์ค๊ฒ ํ๊ณ ์คํฌ๋กค๋ง์ด ์์๋ ๋ true๋ก ๋ฐ๊ฟ ์์น๋ฐ๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๋ค๋น๊ฒ์ด์
์์ดํ
์ ์ ์ฉ๋์ด ์คํฌ๋กคํ๋ฉด ์ฌ๋ผ์ง๋๋ก ํ์๋ค.
</details>
###
<details>
<summary>์ด๋ฏธ์ง ์บ์ ์ฑ๊ธํค ๊ฐ์ฒด </summary>
์ํ ๋ฆฌ์คํธ ๋ทฐ์์ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๊ธฐ ์ํด DataTask ์์
์ UIImageView์ extension์ผ๋ก ํ์ฅํ์ฌ ์ฌ์ฉํ๊ณ ์์๋ค.
```swift
extension UIImageView {
func setImageUrl(_ url: String) {
DispatchQueue.global(qos: .background).async {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, result, error) in
guard error == nil else {
DispatchQueue.main.async { [weak self] in
self?.image = UIImage()
}
return
}
DispatchQueue.main.async { [weak self] in
if let data = data, let image = UIImage(data: data) {
self?.image = image
}
}
}.resume()
}
}
```
๋ค๋ง ์์
์ค ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ฅผ ์๊ฐํ๋ค.
1. ๋ฐ์ดํฐ๋ฅผ loadํ๊ธฐ ์ํด dataTask์ฝ๋๋ฅผ ํ์ฌ ํ์ฅํ๊ณ ์๋๋ฐ ๋ชจ๋ UIImageView๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋๊ฒ ์๋๋ค.
๋ฐ๋ผ์ ๊ธฐ์กด์ ๋ชจ๋ UIImageView๋ฅผ ๋์์ผ๋ก ํ์ฅํ๋ ๋ฐฉ์์์ UIImageView๋ฅผ ์์๋ฐ๋ ์๋ก์ด ๋ฐ์ดํฐ ํ์
์ ๋ง๋ค์๋ค.
```swift
final class DownloadableUIImageView: UIImageView {
var dataTask: URLSessionDataTask?
func setImageUrl(_ url: String) {
guard let url = URL(string: url) else { return }
self.image = UIImage()
self.dataTask = URLSession.shared.dataTask(with: url) { (data, result, error) in
guard error == nil else {
DispatchQueue.main.async { [weak self] in
self?.image = UIImage()
}
return
}
DispatchQueue.main.async { [weak self] in
if let data = data, let image = UIImage(data: data) {
self?.image = image
}
}
}
self.dataTask?.resume()
}
func cancelImageDownload() {
dataTask?.cancel()
dataTask = nil
}
}
```
๊ทธ๋ฌ๋ ์ด ๋ถ๋ถ์์๋ ์ข ๋ ๊ทผ๋ณธ์ ์ธ ๊ณ ๋ฏผ์ ํ๊ฒ ๋์๋ค. "๊ณผ์ฐ UIImageView๊ฐ ๋คํธ์ํฌ ํต์ ์ฝ๋๋ฅผ ์์ ํ๋๊ฒ ๋ง์๊น? UIImageView๋ ๋ง ๊ทธ๋๋ก UI์ ์ฐ์ด๋ ์ด๋ฏธ์ง ๋ทฐ ๊ด๋ จ ์ฝ๋๋ง ์์งํด์ผํ์ง ์์๊น?"
๊ฒฐ๊ตญ ์บ์ฑ ์์
์ ์ถ๊ฐํ๋ฉด์ UIImageView์์ ๋คํธ์ํฌ ํต์ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๋ ์์
์ ํ๋ฒ ๋ ์ํํ๋ค.
```swift
final class ImageCache {
static let shared = ImageCache()
private init() {}
private let cachedImages = NSCache<NSURL, UIImage>()
private var waitingRespoinseClosure = [NSURL: [(UIImage) -> Void]]()
private var dataTasks = [NSURL: URLSessionDataTask]()
private func image(url: NSURL) -> UIImage? {
return cachedImages.object(forKey: url)
}
func load(url: NSURL, completion: @escaping (UIImage?) -> Void) {
if let cachedImage = image(url: url) {
DispatchQueue.main.async {
completion(cachedImage)
}
return
}
if waitingRespoinseClosure[url] != nil {
return
} else {
waitingRespoinseClosure[url] = [completion]
}
let urlSession = URLSession(configuration: .ephemeral)
let task = urlSession.dataTask(with: url as URL) { data, response, error in
guard let responseData = data,
let image = UIImage(data: responseData),
let blocks = self.waitingRespoinseClosure[url], error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
self.cachedImages.setObject(image, forKey: url, cost: responseData.count)
for block in blocks {
DispatchQueue.main.async {
block(image)
}
}
return
}
dataTasks[url] = task
dataTasks[url]?.resume()
}
func cancel(url: NSURL) {
dataTasks[url]?.cancel()
dataTasks[url] = nil
dataTasks.removeValue(forKey: url)
waitingRespoinseClosure[url] = []
waitingRespoinseClosure.removeValue(forKey: url)
}
}
```
์บ์์ ์กด์ฌํ๋ ์ด๋ฏธ์ง๋ผ๋ฉด ๋คํธ์ํฌ ์์ฒญ์ ์ทจ์ํ๋๋ก ํ๊ณ , ๋์ผํ URL์ ์ด๋ฏธ์ง๋ผ๋ ํ์ฌ ๋คํธ์ํฌ ์์ฒญ ์ค์ธ์ง, ์๋ฃํ์ฌ ์บ์์ ์กด์ฌํ๋์ง ๋ฑ ๊ฐ๊ฐ์ ๊ฒฝ์ฐ๋ง๋ค ์ค๋ณต ์์
์ ํผํ๋๋ก ์ค๊ณํด๋ณด์๋ค. ๊ฐ URL ์ ๋ฐ๋ผ ๋ฐ์ดํฐ ํ์คํฌ, ์๋ฃ์ ํด๋ก์ , ์บ์๋ฅผ ๋์๋ค.
</details>
###
<details>
<summary>UITextView์ ํฌ๊ธฐ๊ฐ ๋์ด๋์ง ์๋ ๋ฌธ์ </summary>
<img src="https://i.imgur.com/KXhDRWW.png" width="250" height="500"/>
UITextView๊ฐ ์ํ StackView์ bottomAnchor๋ฅผ ScrollView์ bottomAnchor์ constraint๋ฅผ ๊ฐ๊ฒ ๋ง์ถ์ด ์ฃผ์์์๋ ๋์ด๋์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
<img src="https://i.imgur.com/m9ZM32G.png" width="250" height="500"/>
์ view Hierarchy์์ ๋ณด๋ฏ StackView์ ํฌ๊ธฐ ์์ฒด๊ฐ ๋์ด๋์ง ์๋ ๊ฒ์ ํ์ธํ๋ค. ์ธ๋ก๋ก ์คํฌ๋กค์ด ๋์ด์ผํ๋ ํน์ฑ์ ์ฃผ์ด์ผํ๊ธฐ ๋๋ฌธ์ StackView์ topAnchor, bottonAnchor๋ฅผ contentLayoutGuide์ constraintํ ๊ฒ์ด ๋ฌธ์ ๊ฐ ๋์๋ค๊ณ ํ๋จํ๋ค.
๋ฐ๋ผ์, StackView์ heightAnchor๋ฅผ ์ง์ ํด์ฃผ์ด ํด๊ฒฐํ๋ค.
</details>
###
<details>
<summary> ๋ฐฐ๋ ๋ทฐ์ ์ด๋ฏธ์ง๊ฐ ๋ฌดํ ๋ฐ๋ณตํ๋๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ
</summary>
์ด๋ฏธ์ง์ ๋ง์ง๋ง ์ธ๋ฑ์ค์์ ๋ค์ ์ฒ์ ์ธ๋ฑ์ค๋ก ๋์ด๊ฐ๋ ๋ก์ง์ ๋ํด์ ๊ณ ๋ฏผํ์๋ค.

ํด๊ฒฐ ๋ฐฉ๋ฒ์ผ๋ก๋ ์ฒซ๋ฒ์งธ ์ด๋ฏธ์ง ๋ฐ ๋ง์ง๋ง ์ด๋ฏธ์ง์ ์ด๋ฏธ์ง ๋ทฐ๋ฅผ ์ถ๊ฐํ๋ค์ ํด๋น ์ด๋ฏธ์ง ๋ทฐ์ ๋ค์์ ์ฌ ์ด๋ฏธ์ง๋ฅผ ์ถ๊ฐํด์ฃผ๊ณ ๊ทธ ์ด๋ฏธ์ง๊ฐ ํ๋ฉด์ ๋์ฌ๋ scrollView์ contentOffset์ ํด๋น ์ด๋ฏธ์ง์ ์๋ ์์น๋ก ์ด๋์ํจ๋ค. ๊ทธ๋ ๊ฒ ๋๋ฉด ์ฌ์ฉ์์
์ฅ์์๋ ์ด์ง๊ฐ์ ๋๋ผ์ง ์๊ณ ๋ฌดํ ์คํฌ๋กค์ด ๋๋ค๋ ์ฐฉ๊ฐ์ ํ๊ฒ ๋๋ค.
</details>
###
<details>
<summary>
ํ๋ฉด ์ด๋๊ฐ์ ๋ฆฌ์คํธ ์
์์น ์ด๋ ๋ฌธ์
</summary>
๋ฆฌ์คํธ ๋ทฐ์ ํน์ ์์น์์ ํน์ ์
์ ๋ํ ์์ ์ด๋, ์ญ์ ๊ฐ ์ด๋ฃจ์ด์ง๋ ํด๋น ์์
์ดํ ๋ค์ ์
๋ก ๋์ ์ฌ๋ ์์น๊ฐ ๋ณ๊ฒฝ๋๋ ๋ฌธ์ ๊ฐ ์์๋ค.


ํ๋ฉด์ด๋๊ฐ์ ํด๋น ์
์ indexPath ๊ฐ์ ํ ๋น ๋ฐ์๋ค์ ํ ๋น ๋ฐ์ indexPath ์์น๋ก ์คํฌ๋กคํด์ฃผ์๋ค.
</details>
###
<details>
<summary>
RegistView Image ์ญ์ ํ๋ ๋ฐฉ๋ฒ
</summary>
CollectionView๋ก ์ด๋ฏธ์ง ์ถ๊ฐ๋ง ๊ตฌํํ ์ํ์์ "X"๋ฒํผ์ ๋ง๋ค์ด ์ญ์ ๋ฅผ ๊ตฌํํด์ผ๋๋ค.
๊ฐ Cell์ ๊ตฌํ๋ "X"๋ฒํผ์ ์ก์
์ ๋ฃ๋ ๋ฐฉ๋ฒ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
์ญ์ ๋ฒํผ์ ๋๋ฅธ index๋ฅผ ๊ตฌํ ์ ์์๋ค.
์๋ํ๋ฉด DiffableDataSource๋ฅผ ์ฌ์ฉํ๊ณ ์์๊ธฐ ๋๋ฌธ์ด๋ค. DiffableDataSource๋ indexPath๊ฐ ์๋ ์ง์ ๋ ํ์
์ผ๋ก ์๊ธฐ ๋๋ฌธ์ index๋ฅผ ์ด์ฉํ๋ ๊ฒ์ DiffableDataSource์ ํน์ง์ ์ด์ฉํ์ง ๋ชปํ๋ค๊ณ ์๊ฐํ๋ค.
๊ทธ๋์ ๊ฐ Cell์ ์ง์ ํ ๋ ํด๋ก์ ๋ฅผ ์ด์ฉํ์ฌ Action์ ๋ฃ์ด์ฃผ๊ธฐ๋ก ํ๋ค.
```swift
let cell = UICollectionView.CellRegistration<ProductRegistCollectionViewCell, UIImage> { cell, indexPath, item in
cell.removeImage = {
self.deleteDataSource(image: item)
}
cell.configureImage(data: item)
}
```
์ ์ฝ๋์ ๊ฐ์ด ๊ฐ cell์ item์ ์ง์ ํ ๋ ๊ทธ item์ dataSource์์ ์ง์ฐ๋ action์ ํด๋ก์ ๋ก ์ด์ฉํ์ฌ ๋๊ฒจ์ฃผ๊ฒ ๋๋ค.
```swift
@objc private func didTapRemoveButton() {
removeImage?()
}
```
์ ์ฝ๋์ ๊ฐ์ด ๊ฐ Cell์ ์ง์ ๋ "X"๋ฒํผ action์ ํด๋ก์ ๋ฅผ ์ถ๊ฐํด์ฃผ์ด Delete๊ธฐ๋ฅ์ ๊ตฌํํ๋ค.
</details>