Giter Club home page Giter Club logo

ios-open-market-refact's Introduction

My Market ๐Ÿช (MVVM + UIKit)

<ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„>

  • 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 ๊ตฌํ˜„
์ƒํ’ˆ ๋“ฑ๋ก ์ƒํ’ˆ ์ˆ˜์ • ์ƒํ’ˆ ์‚ญ์ œ

๐Ÿš€ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

MVC

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>

MVVM

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๊ด€๋ จ ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ๋ทฐ ๋ชจ๋ธ์—์„œ ์„ ์–ธํ•˜๋Š”๊ฒƒ ์ž์ฒด๊ฐ€ ๋ชจ์ˆœ์ด๋ผ ์ƒ๊ฐํ•˜์—ฌ ์ด๋Ÿฐ์‹์œผ๋กœ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.


RegistViewModel subscribe์‹œ ์ดˆ๊ธฐ ์‹คํ–‰ ๋ฌธ์ œ ์ดˆ๊ธฐ ๊ฐ’์ด ์„ค์ •๋˜๊ณ  ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค output์„ ์ด์šฉํ•˜์—ฌ post, patchํ•ด์•ผ๋˜๋Š”๋ฐ, ์ดˆ๊ธฐ ๊ฐ’์œผ๋กœ๋„ post๊ฐ€ ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. Enum ํƒ€์ž…์„ ์ƒˆ๋กœ ๋งŒ๋“ค์–ด ์ดˆ๊ธฐ์—๋งŒ unUpdatable case๋ฅผ ๋„ฃ์–ด์ฃผ์–ด ์ดˆ๊ธฐ์—๋งŒ ๋„คํŠธ์›Œํ‚น์„ ํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋„ฃ์—ˆ์ง€๋งŒ ๋ถˆํ•„์š”ํ•œ ํƒ€์ž…์ด ์ถ”๊ฐ€๋˜๊ณ , ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹Œ ์ •๋ณด๋ฅผ viewModel์— ๋„˜๊ฒจ์ค€๋‹ค๊ณ  ์ƒ๊ฐํ•˜์˜€๋‹ค. ๊ทธ๋ž˜์„œ input์— ๋“ค์–ด๊ฐ€๋Š” Observable์˜ RegistrationProductํƒ€์ž…์„ ์˜ต์…”๋„๋กœ ์„ค์ •ํ•˜์—ฌ nil์ผ ๊ฒฝ์šฐ closure์„ returnํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋„˜๊ธฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค.

ios-open-market-refact's People

Contributors

kiwi1023 avatar yusw10 avatar apwierk2451 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.