<ํ๋ก์ ํธ ๊ธฐ๊ฐ>
- MVC: 2022-11-10 ~ 2022-12-02
- MVVM: 2022-12-05 ~ 2022-12-20
์ก๊ธฐ์, ์ ํ์, ์ด์์ฐฌ
์๋ฒ์์ ๋คํธ์ํน์ ํตํด ์ํ์ ๋ฑ๋ก, ์์ , ์ญ์ ๊ฐ ๊ฐ๋ฅํ ๋๋ง์ ๋ง์ผ
UIKit
Network
URLSession Mock Test
Json Decoding Strategy
NSCache
XCTestExpection
completionHandler
Escaping Closure
URLSession
RefreshController
Test Double
UICollectionView
DiffableDataSource
CompositionalLayout
MVVM
delegate
Observable
๋ฉ์ธํ๋ฉด (๋ฐฐ๋๋ทฐ) | ๋ฌดํ์คํฌ๋กค | UISearch Bar ๊ตฌํ |
---|---|---|
์ํ ๋ฑ๋ก | ์ํ ์์ | ์ํ ์ญ์ |
Launch Screen ์ด์
์ด๊ธฐ CollectionView๋ฅผ ์ค์ ํ๋ฉด์ ํ๋ฉด์ ํ์ธํด๋ณด์๋๋ฐ ๋ค์๊ณผ ๊ฐ์ด ์ํ๋จ์ ์์ญ์ด ์๋ ค์ ๋์ค๋๊ฑธ ํ์ธํ ์ ์์๋ค.
CollectionView์ ๊ฐ anchor๋ฅผ ๋ฉ์ธ View Controller์ View์ safeAreaLayoutGuide์ ๋ง์ถฐ์ฃผ์ง ์์๋ค๊ณ ์๊ฐ๋์ด ๋ทฐ ๊ณ์ธต ์ฐฝ์ ๋ณด์๋๋ฐ ์คํ๋ ค ๋ทฐ ๊ณ์ธต์์์๋ ์ ํ ๋ฌธ์ ๊ฐ ์์๋ค.
์๋ ธ๋ค๊ธฐ ๋ณด๋ค๋ ์์ window์์ฒด๊ฐ ์๊ฒ ์กํ์๋ค๋ ๊ฒ์ ๊ฐ๊น์ด ํํ์๋ค.
๋ฌธ์ ๋ ์์ ํ์ง ๋ชปํ๊ณณ์์ ๋ฐ์ํ๊ณ ์์๋ค. ํ๋ก์ ํธ์ UI๋ฅผ ์ฝ๋๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝํ๋ฉด์ ๊ธฐ๋ณธ์ผ๋ก ์์ฑ๋๋ ์คํ ๋ฆฌ๋ณด๋ ํ์ผ๋ค์ ๋ชจ๋ ์ ๊ฑฐํ๋ ๊ณผ์ ์ ๊ฑฐ์ณค๋ค.
๊ทธ ๊ณผ์ ์ค์ LaunchScreen์ ์์ฑํ๋ ์ต์ ์ ๊ป๋๋ฐ ์ด ์ต์ ์ ๊บผ๋ฒ๋ฆฌ๋ ์์ ๊ฐ์ด window ์์ฒด๊ฐ ์๊ฒ ์กํ๋ค.
์ด ์ต์ ์ ๋ค์ ํด์ผ๋ก์จ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์์ง๋ง, ์ ํํ ์ด๋ค ์๋ฆฌ๋ก ์ด์๊ฐ์ด ๋์๋์๋์ง ๊ด๋ จ ๊ธ์ด ๋ถ์กฑํด์ ์ ์ ์์๋ค...(๋ฐ์น ์คํฌ๋ฆฐ์ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ๊ธ์ด ์ฃผ๋ฅ์๋ค)
๋ค๋ง ์์ํด๋ณด์๋ฉด, SceneDelegate์์ ์๋์ฐ๋ฅผ ์ธ์คํด์คํ ํ๋ ๊ณผ์ ์์ ๊ธฐ์กด์๋ ๋ฐ์น์คํฌ๋ฆฐ์ด ํ๋ฉด ์ ์ฒด ํฌ๊ธฐ์ ๋ง๊ฒ ์ต์์ Frame์ ์ก๊ณ ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก windowํฌ๊ธฐ๊ฐ ์กํ์์ง๋ง, ์ฐ๋ฆฌ์ ์ฝ๋์์๋ ์ด ๊ณผ์ ์ด ์๋ต๋์ด์ ์ปจํ ์ธ ์ต์ ํฌ๊ธฐ๋๋ก ์๋์ฐ๊ฐ ์ค์ ๋๊ฒ์ผ๋ก ์ถ์ธกํ๋ค. ์๋ง๋ SceneDelegate์์ Scene์ ์์ฑ์์ ์๋์ฐ ํฌ๊ธฐ๋ฅผ ์คํฌ๋ฆฐ ํฌ๊ธฐ๋ก ์ง์ ํด์ค๋ค๋ฉด ์๋์ฐ ํฌ๊ธฐ๊ฐ ์๋ํ ๋๋ก ๋์ค์ง ์์๊น ์ถ๋ค.
์ํ ๋ชฉ๋ก ํ๋ฉด์ด๋์ ์์น๋ฐ๊ฐ ๋ณด์ฌ์ง๋๋ก ์์
์ํ ๋ชฉ๋ก ํ๋ฉด์ผ๋ก ์ง์ ์ ๋ค์๊ณผ ๊ฐ์ด ์์น๋ฐ๊ฐ ๋ณด์ฌ์ง๋๋ก ์ค์ ํ๊ณ ์ ํ๋ค.
์ด๋ฅผ NavigationItem์ hidesSearchBarWhenScrolling
์์ฑ์ ํตํด ์ง์ ํ๊ณ ์ ํ๋๋ฐ ๋ทฐ ์ง์
์ ์์น๋ฐ๊ฐ ๋ณด์ด๊ฒ ํ๊ธฐ ์ํด ์ด ์์ฑ์ false๋ก ํ๋ฉด ์คํฌ๋กค์ ์์ฐ์ค๋ฝ๊ฒ ์ฌ๋ผ์ง์ง ์์๋ค.
๋ฐ๋ผ์ ๋ทฐ ์ต์ด ์ง์ ํ์ฌ ViewWillAppear์์ ์ด๋ฅผ ํด์ ํ์ฌ ์์น๋ฐ๊ฐ ๋์ค๊ฒ ํ๊ณ ์คํฌ๋กค๋ง์ด ์์๋ ๋ true๋ก ๋ฐ๊ฟ ์์น๋ฐ๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๋ค๋น๊ฒ์ด์ ์์ดํ ์ ์ ์ฉ๋์ด ์คํฌ๋กคํ๋ฉด ์ฌ๋ผ์ง๋๋ก ํ์๋ค.
์ด๋ฏธ์ง ์บ์ ์ฑ๊ธํค ๊ฐ์ฒด
์ํ ๋ฆฌ์คํธ ๋ทฐ์์ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๊ธฐ ์ํด DataTask ์์
์ UIImageView์ extension์ผ๋ก ํ์ฅํ์ฌ ์ฌ์ฉํ๊ณ ์์๋ค.
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๋ฅผ ์์๋ฐ๋ ์๋ก์ด ๋ฐ์ดํฐ ํ์ ์ ๋ง๋ค์๋ค.
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์์ ๋คํธ์ํฌ ํต์ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๋ ์์ ์ ํ๋ฒ ๋ ์ํํ๋ค.
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 ์ ๋ฐ๋ผ ๋ฐ์ดํฐ ํ์คํฌ, ์๋ฃ์ ํด๋ก์ , ์บ์๋ฅผ ๋์๋ค.
UITextView์ ํฌ๊ธฐ๊ฐ ๋์ด๋์ง ์๋ ๋ฌธ์
UITextView๊ฐ ์ํ StackView์ bottomAnchor๋ฅผ ScrollView์ bottomAnchor์ constraint๋ฅผ ๊ฐ๊ฒ ๋ง์ถ์ด ์ฃผ์์์๋ ๋์ด๋์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
์ view Hierarchy์์ ๋ณด๋ฏ StackView์ ํฌ๊ธฐ ์์ฒด๊ฐ ๋์ด๋์ง ์๋ ๊ฒ์ ํ์ธํ๋ค. ์ธ๋ก๋ก ์คํฌ๋กค์ด ๋์ด์ผํ๋ ํน์ฑ์ ์ฃผ์ด์ผํ๊ธฐ ๋๋ฌธ์ StackView์ topAnchor, bottonAnchor๋ฅผ contentLayoutGuide์ constraintํ ๊ฒ์ด ๋ฌธ์ ๊ฐ ๋์๋ค๊ณ ํ๋จํ๋ค. ๋ฐ๋ผ์, StackView์ heightAnchor๋ฅผ ์ง์ ํด์ฃผ์ด ํด๊ฒฐํ๋ค.
๋ฐฐ๋ ๋ทฐ์ ์ด๋ฏธ์ง๊ฐ ๋ฌดํ ๋ฐ๋ณตํ๋๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ
์ด๋ฏธ์ง์ ๋ง์ง๋ง ์ธ๋ฑ์ค์์ ๋ค์ ์ฒ์ ์ธ๋ฑ์ค๋ก ๋์ด๊ฐ๋ ๋ก์ง์ ๋ํด์ ๊ณ ๋ฏผํ์๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ์ผ๋ก๋ ์ฒซ๋ฒ์งธ ์ด๋ฏธ์ง ๋ฐ ๋ง์ง๋ง ์ด๋ฏธ์ง์ ์ด๋ฏธ์ง ๋ทฐ๋ฅผ ์ถ๊ฐํ๋ค์ ํด๋น ์ด๋ฏธ์ง ๋ทฐ์ ๋ค์์ ์ฌ ์ด๋ฏธ์ง๋ฅผ ์ถ๊ฐํด์ฃผ๊ณ ๊ทธ ์ด๋ฏธ์ง๊ฐ ํ๋ฉด์ ๋์ฌ๋ scrollView์ contentOffset์ ํด๋น ์ด๋ฏธ์ง์ ์๋ ์์น๋ก ์ด๋์ํจ๋ค. ๊ทธ๋ ๊ฒ ๋๋ฉด ์ฌ์ฉ์์ ์ฅ์์๋ ์ด์ง๊ฐ์ ๋๋ผ์ง ์๊ณ ๋ฌดํ ์คํฌ๋กค์ด ๋๋ค๋ ์ฐฉ๊ฐ์ ํ๊ฒ ๋๋ค.
ํ๋ฉด ์ด๋๊ฐ์ ๋ฆฌ์คํธ ์ ์์น ์ด๋ ๋ฌธ์
๋ฆฌ์คํธ ๋ทฐ์ ํน์ ์์น์์ ํน์ ์ ์ ๋ํ ์์ ์ด๋, ์ญ์ ๊ฐ ์ด๋ฃจ์ด์ง๋ ํด๋น ์์ ์ดํ ๋ค์ ์ ๋ก ๋์ ์ฌ๋ ์์น๊ฐ ๋ณ๊ฒฝ๋๋ ๋ฌธ์ ๊ฐ ์์๋ค.
ํ๋ฉด์ด๋๊ฐ์ ํด๋น ์ ์ indexPath ๊ฐ์ ํ ๋น ๋ฐ์๋ค์ ํ ๋น ๋ฐ์ indexPath ์์น๋ก ์คํฌ๋กคํด์ฃผ์๋ค.
RegistView Image ์ญ์ ํ๋ ๋ฐฉ๋ฒ
CollectionView๋ก ์ด๋ฏธ์ง ์ถ๊ฐ๋ง ๊ตฌํํ ์ํ์์ "X"๋ฒํผ์ ๋ง๋ค์ด ์ญ์ ๋ฅผ ๊ตฌํํด์ผ๋๋ค. ๊ฐ Cell์ ๊ตฌํ๋ "X"๋ฒํผ์ ์ก์ ์ ๋ฃ๋ ๋ฐฉ๋ฒ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. ์ญ์ ๋ฒํผ์ ๋๋ฅธ index๋ฅผ ๊ตฌํ ์ ์์๋ค. ์๋ํ๋ฉด DiffableDataSource๋ฅผ ์ฌ์ฉํ๊ณ ์์๊ธฐ ๋๋ฌธ์ด๋ค. DiffableDataSource๋ indexPath๊ฐ ์๋ ์ง์ ๋ ํ์ ์ผ๋ก ์๊ธฐ ๋๋ฌธ์ index๋ฅผ ์ด์ฉํ๋ ๊ฒ์ DiffableDataSource์ ํน์ง์ ์ด์ฉํ์ง ๋ชปํ๋ค๊ณ ์๊ฐํ๋ค. ๊ทธ๋์ ๊ฐ Cell์ ์ง์ ํ ๋ ํด๋ก์ ๋ฅผ ์ด์ฉํ์ฌ Action์ ๋ฃ์ด์ฃผ๊ธฐ๋ก ํ๋ค.
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์ ํด๋ก์ ๋ก ์ด์ฉํ์ฌ ๋๊ฒจ์ฃผ๊ฒ ๋๋ค.
@objc private func didTapRemoveButton() {
removeImage?()
}
์ ์ฝ๋์ ๊ฐ์ด ๊ฐ Cell์ ์ง์ ๋ "X"๋ฒํผ action์ ํด๋ก์ ๋ฅผ ์ถ๊ฐํด์ฃผ์ด Delete๊ธฐ๋ฅ์ ๊ตฌํํ๋ค.
</details>
Observable ํ์ ์ ์ค์
์ด๋ฒคํธ ํ๋ฆ์ ๋จ๋ฐฉํฅ์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ด๋ฐ์ ํด๋ก์ ์ ํํ๋ก ๊ตฌํ์ ํ์๋ค. ํ์ง๋ง ์ด๋ฒคํธ์ ๊ฐฏ์ ๋งํผ ๋ทฐ๋ชจ๋ธ์ด ํด๋ก์ ์ ๋ฐ์ดํฐ ๋ชจ๋ธ์ ์์ ํ๊ณ ์์ด์ผ ํ๊ธฐ์ ์ด๋ฅผ ํฉ์น๋ ๊ฐ๋ ์ด ํ์ํ๋ค.
RxSwift์ Observable ํ์ ์ ์ฐธ๊ณ ํ์ฌ ๋ค์๊ณผ ๊ฐ์ด ๋ฆฌ์ค๋๋ฅผ ์์ ํ๋ ์ปค์คํ Observable ํ์ ์ ์์ฑํ์ฌ ์ด๋ฅผ ํด๊ฒฐํด๋ณด์๋ค.
class Observable<T> {
var value: T {
didSet {
self.listener?(value)
}
}
var listener: ((T) -> Void)?
init(_ value: T) {
self.value = value
}
func subscribe(listener: @escaping (T) -> Void) {
listener(value)
self.listener = listener
}
}
์ด Observable ํ์ ์ ํ์ฉํจ์ผ๋ก์จ ViewModel์ Input๊ณผ Output์ผ๋ก ์ด๋ฒคํธ ์ ์ถ๋ ฅ์ ์ ๋ฆฌํ๊ณ ์ต์ข ์ ์ผ๋ก View Controller์์ ์ํ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ์ด์ ๋์๋๋ ๋ฐ์ดํฐ๋ชจ๋ธ์ ํด๋ก์ ๋ก ๋๊ฒจ์ฃผ์ด RxSwift์ bindindg์์ ๊ณผ ์ ์ฌํ๊ฒ UIKit๋ง์ ์ฌ์ฉํ์ฌ ๊ตฌํํ ์ ์์๋ค.
๋ทฐ ๋ชจ๋ธ์ Input์ ์ด๋ค ๊ฐ์ ๋ฃ์ด์ผ ํ ์ง์ ๋ํ์ฌ
RxSwift์ ๊ฒฝ์ฐ UIComponents์ rx๋ฅผ ์ด์ฉํ์ฌ ์ ๊ทผํ๊ณ ์ด์ ๋ํ ์ด๋ฒคํธ ํ๋ฆ์ ์ด๋์ด ์ฌ ์ ์๋ค. ๋ค๋ง Pure MVVM์ ๋ชฉํ๋ก ๊ฐ๋ฐ์ ํ๋ค๋ณด๋ ์ด ์ด๋ฒคํธ ํ๋ฆ์ ์์์ ์ด ์ด๋์ ์์นํ ๊ฒ์ธ๊ฐ์ ๋ํด์ ๊ต์ฅํ ๊ณ ๋ฏผ์ ๋ง์ด ํ๋ค.
Main ViewController์ ๊ฒฝ์ฐ ์ด๊ธฐ ๋ฐ์ดํฐ ๋ก๋๋ง ๋ถ๋ฌ์ค๊ธฐ ๋๋ฌธ์ binding ์์ ์์ Observable ํ์ ์ ๋๊ฒจ ์ฃผ์๋ค.
// MainViewController.swift
private func bind() {
let miniListFetchAction = Observable<(InitialPageInfo)></(InitialPageInfo)>(MainViewControllerNameSpace.initialPageInfo)
let output = mainViewModel.transform(input: .init(
pageInfoInput: miniListFetchAction
))
output.fetchedProductListOutput.subscribe { list in
DispatchQueue.main.async {
self.updateDataSource(data: list)
}
}
}
ํ์ง๋ง ์ํ ๋ชฉ๋ก ๋ทฐ์์๋ ํ์ฌ ๋ถ๋ฌ์จ ํ์ด์ง์ ํ์ด์ง๋น ์์ดํ
์ ๊ฐ์, ํ์ฌ ์ํ๊ฐ update์ธ์ง ํน์ add์ธ์ง์ ๋ํด์ ๋ณํ๋ฅผ ViewController๊ฐ ์์ ํ๊ณ ์์ด์ผ ํด์ ViewController์ pageState
๋ผ๋ Observableํ์
์ ํ๋กํผํฐ๋ก ์์ ํ๋๋ก ํ์๋ค. ๋ํ SearchController๋ฅผ ํตํด ์
๋ ฅ๋๋๊ฒ๋ ํํฐ๋ง ๊ฐ์ ์ด๋ฒคํธ ๋ณํ๋ก ์ฐ๊ฒฐํ๊ธฐ ์ํด Observableํ์
์ผ๋ก ๋ง๋ค์๋ค.
//ProductListViewController.swift
private let pageState = Observable<(
pageNumber: Int,
itemsPerPage: Int,
fetchType: FetchType)
>((
ProductListViewControllerNameSpace.initialPageInfo.pageNumber,
ProductListViewControllerNameSpace.initialPageInfo.itemsPerPage,
.update
))
private let filteringState = Observable<String>("")
์ด ํ๋กํผํฐ ๋๊ฐ์ง๋ฅผ ๋ทฐ ๋ชจ๋ธ์ transform ๋ฉ์๋๋ฅผ ํตํด ๋ฐ์ธ๋ฉ ์์ ์ ๊ฑฐ์ณค๋ค.
let output = productListViewModel.transform(input: .init(
productListPageInfoUpdateAction: pageState,
filteringStateUpdateAction: filteringState
))
๋ทฐ๋ชจ๋ธ์ ์๋ฌ ํธ๋ค๋ง์ ์ฒ๋ฆฌ์ ๊ดํ์ฌ
๊ธฐ์กด์ ์ฝ๋๋ ์๋ฌ ํธ๋ค๋ง์ด ๋คํธ์ํฌ ์ฝ๋์ ์์ ๋์ด ์์๋ค. SessionProtocol๋ก ๋ถํฐ ResultType์ ๋ฐํ๋ฐ์ ์ด๋ฅผ ๋ถ๊ธฐ ์ฒ๋ฆฌํ์ฌ Completion Handler๋ฅผ ๋๊ฒจ์ฃผ๋ ๋ฐฉ์์ด์๋ค.
ํ์ง๋ง ๊ฒฐ๊ณผ์ ๋ถ๊ธฐ ์ฒ๋ฆฌ ์์ฒด๋ฅผ ๋ทฐ ์ปจํธ๋กค๋ฌ๊ฐ ์งํํ๊ฒ ๋๋ ๊ฒ ์์ฒด๊ฐ ๋น์ฆ๋์ค ๋ก์ง์ด๋ผ๊ณ ์๊ฐ๋์๋ค. ๋ํ ์คํจ์ ๊ฒฝ์ฐ AlertDirector๋ฅผ ํตํด ์คํจ ๋ด์ฉ์ ์ถ๋ ฅ ํด์ผํ๋๋ฐ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ทฐ๋ชจ๋ธ๋ก ์ด๊ด๋๋ฉด์ ์๋ฌ ํ์ ์ ๋๊ฒจ ๋ฐ๋๋ก ๊ตฌ์กฐ๋ฅผ ์ง์ผํ๋ค.
https://benoitpasquier.com/error-handling-swift-mvvm/
์ ๋ธ๋ก๊ทธ๋ฅผ ์ฐธ๊ณ ํ์ฌ ์๋ฌ์ฝ๋์ ๋ํด์ ๋ถ๋ฆฌ๋ฅผ ํ๋ค.
๋จผ์ ๋ทฐ๋ชจ๋ธ ๋ด๋ถ์ ์๋ฌ ํธ๋ค๋ง์ ์ค์ ๋ก ์งํ ํ ํด๋ก์ ๋ฅผ ์ ์ธํ์๋ค.
var onErrorHandling : ((APIError) -> Void)?
๊ทธ๋ฆฌ๊ณ bind ๋ฉ์๋์์ ์ค์ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ๋์์ค AlertDirector๋ฅผ ํธ์ถํ๋๋ก ํ์๋ค.
mainViewModel.onErrorHandling = { failure in
AlertDirector(viewController: self).createErrorAlert(
message: MainViewControllerNameSpace.getDataErrorMassage
)
}
๋ทฐ ๋ชจ๋ธ์์๋ ์๋ฌ์ ๋ถ๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์งํํ๋ค. success๋ผ๋ฉด Completion Handler๋ฅผ ํธ์ถํ๊ณ , failure๋ผ๋ฉด ViewController๋ก ๋ถํฐ ์ฃผ์ ๋ฐ์ ํด๋ก์ ๋ฅผ ์คํํ๋ค.
input.pageInfoInput.subscribe { (pageNumber: Int, itemsPerPage: Int) in
self.fetchProductList(pageNumber: pageNumber, itemsPerPage: itemsPerPage) { (result: Result<[Product], APIError>) in
switch result {
case .success(let productList):
fetchedProductListOutput.value = productList
case .failure(let failure):
self.onErrorHandling?(failure)
}
}
}
์ด๋ ๊ฒํ์ฌ ViewController๋ ์คํจ์ ๋์ํด์ผํ View์ ๊ด๋ จ๋ ๋ก์ง๋ง์ ์์ ํ๊ฒ ๋๊ณ , ViewModel์์ ์๋ฌ ์ฒ๋ฆฌ๋ถ๊ธฐ์ ๊ด๋ จ๋ ๋น์ฆ๋์ค ๋ก์ง์ ๋ฐฐ์นํจ๊ณผ ๋์์ View๊ด๋ จ ์์ ์ ํ์ง ์๋๋ก ํ ์ ์์๋ค.
View Model ๋ด๋ถ์ ๋ฐ์ดํฐ์ ํํ
์ต์ด์ Input Output ๊ตฌ์กฐ๋ฅผ ์์ฑํ ๋ ๋ทฐ ๋ชจ๋ธ ๋ด๋ถ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐฐ์น์ํค์ง ์์ผ๋ ค๊ณ ํ๋ค. ViewController์ ์ํ ๋ณํ์ ๋ฐ๋ฅธ ๋น์ฆ๋์ค ๋ก์ง์ ๊ฑฐ์ณ ํธ๋ค๋ง๋ ๋ฐ์ดํฐ๋ฅผ ๋๊ฒจ์ฃผ๊ธฐ๋ง ํ๋ ์ญํ ๋ง์ ๋ทฐ ๋ชจ๋ธ์ ๋ถ์ฌํ๋ คํ๋ค.
ํ์ง๋ง ์ํ ๋ชฉ๋ก์ ๊ฒฝ์ฐ Search Controller๋ก ๋ถํฐ ๋์ด์ค๋ ๊ฒ์์ด์ ์ํ๋ณํ์ ๋ฐ๋ผ ๊ฒ์์ ์งํํ ๋ฐ์ดํฐ๊ฐ ํ์ํ๊ธฐ์ ๊ฒฐ๊ตญ ProductListViewModel ๋ด๋ถ์ [Product]
ํ์
์ ์ ์ฅํ๊ฒ ๋์๋ค.
๋ค๋ง dataSource ์์ฒด๋ฅผ ๋ทฐ ๋ชจ๋ธ์ ์์น์ํค๊ฒ ๋๋ฉด ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์๊ฒ์ด๋ผ ์๊ฐํ์๋๋ฐ UIKit๊ด๋ จ ๋ชจ๋ ์ฝ๋๋ฅผ ๋ทฐ ๋ชจ๋ธ์์ ์ ์ธํ๋๊ฒ ์์ฒด๊ฐ ๋ชจ์์ด๋ผ ์๊ฐํ์ฌ ์ด๋ฐ์์ผ๋ก ์์ฑํ๊ฒ ๋์๋ค.