Skip to content

Latest commit

ย 

History

History
496 lines (376 loc) ยท 22.8 KB

File metadata and controls

496 lines (376 loc) ยท 22.8 KB

ChildOfWeather

1. ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 2022.04.01 ~

2. ์ปค๋ฐ‹ ๊ทœ์น™

  • ๋‹จ์œ„: ๊ธฐ๋Šฅ ๋‹จ์œ„
  • ์ปค๋ฐ‹ ์Šคํƒ€์ผ: ์นด๋ฅด๋งˆ ์Šคํƒ€์ผ

3. Steps

  • Step1

์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๋ชจ๋“  ๊ธฐ๋Šฅ ๊ตฌํ˜„
๋ชฉํ‘œ: ์ด์ „๊นŒ์ง€ ์ง„ํ–‰ํ–ˆ๋˜ ํ”„๋กœ์ ํŠธ์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—†์ด ๊ตฌํ˜„ํ•˜์—ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์กด์„ฑ์„ ๋‚ฎ์ถ˜๋‹ค

  • Step2

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๊ตฌํ˜„

  • Step3

RxSwift๋ฅผ importํ•˜์—ฌ ๋‹จ๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ
๋ชฉํ‘œ: RxSwift์™€ RxCocoa ์‚ฌ์šฉ, ํด๋ฆฐ์•„ํ‚คํ…์ณ ๊ทœ์น™ ์ค€์ˆ˜

  • Step4

AppCoordinator๋ฅผ ์‚ฌ์šฉํ•ด ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐ TabBarController ๋ฅผ ํ†ตํ•œ ํ™”๋ฉด์ „ํ™˜
Compositional Layout์„ ์‚ฌ์šฉํ•œ CollectionView ๋ฐ DiffableDataSourceํ™œ์šฉ
RxNimble์„ ํ™œ์šฉํ•œ ViewModel, UseCase ํ…Œ์ŠคํŠธ

4. ํ‚ค์›Œ๋“œ

RxSwift, Clean Architecture MVVM, RxCocoa, Swift Package Manager Coordinator, WebView, Localization

5. ๋ชฉ์ฐจ

6. ์‚ฌ์šฉ

๋„์‹œ ๊ฒ€์ƒ‰ ๊ฒ€์ƒ‰์ฐฝ๋…ธ์ถœ
๋‚ ์”จ ์ •๋ณด ํ˜„์žฌ ์œ„์น˜ ํ‘œ์‹œ

Step1 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๊ธฐ๋ณธ ๊ตฌํ˜„

API ํ†ต์‹  Generic ๊ตฌํ˜„

final class APIService {
     func request<T: Decodable>(
      _ type: RequestType,
      completion: @escaping (Result<T, Error>) -> Void
    ) {

๋‹ค๋ฅธ URL๋กœ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ์ƒํ™ฉ์„ ๋Œ€๋น„ํ•˜์—ฌ Generic์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

ViewModel Delegate ๊ตฌํ˜„

protocol DetailViewModelDelegate: AnyObject {
   func loadWebView(url: URL)
   func loadTodayDescription(weather description: String)
   func loadImageView()
   func cacheImage()
}

ViewModel์€ ViewController๋ฅผ ์•Œ์ง€ ๋ชปํ•˜๊ธฐ์— Delegate ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

Coordinator ๊ตฌํ˜„

final class MainCoordinator {

    private let navigationController: UINavigationController
    private let imageCacheUseCase = ImageCacheUseCase(imageProvideRepository: DefaultImageProvideRepository())
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

๋ทฐ์™€ ๋ทฐ์‚ฌ์ด์˜ ์ด๋™์„ ๋‹ด๋‹นํ•˜๋Š” Coordinator๋ฅผ ๋‘์–ด ๋” ์ด์ƒ ViewController๊ฐ€ ๋ทฐ๋ฅผ ๋„์–ด์ฃผ๋Š” ์—ญํ• ์„ ํ•˜์ง€ ์•Š๊ฒŒ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ Coordinator ์˜ ์ €์žฅํ”„๋กœํผํ‹ฐ์™€ ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์˜ ์ง€์—ญ๋ณ€์ˆ˜๋กœ ๊ฐ ViewController ์™€ ViewModel์˜ ํ”„๋กœํผํ‹ฐ์˜ ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅํ”„๋กœํผํ‹ฐ์™€ ์ง€์—ญ๋ณ€์ˆ˜๋กœ ๋‹ค๋ฅด๊ฒŒ ์ฃผ์ž…์‹œ์ผœ์ฃผ๋Š” ์ด์œ ๋Š” ViewModel ์ด ์บ์‹œ์ฒ˜๋Ÿผ ๊ฐ™์€ Repository์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ”๋ผ๋ณด์•„์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์™€ ๋‹ค๋ฅธ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์„œ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์˜ Repository๋ฅผ ๋ฐ”๋ผ๋ณด์•„์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์บ์‹ฑ ๊ตฌํ˜„

final class DefaultImageProvideRepository: ImageProvideRepository {
   
   let service = CacheService()
   
   func setCache(object: ImageCacheData) {
       let key = object.key
       self.service.cache.setObject(object, forKey: key as NSString)
   }
   
   func getCache(key: String) -> ImageCacheData? {
       self.service.cache.object(forKey: key as NSString)
   }
}

WebView๋กœ ์ง€๋„๋ฅผ ๋„์–ด์ฃผ๊ณ  ์ง€์—ญ์ƒ์„ธ ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ€๋ฉด ์บ์‹ฑ๋˜๋Š” ํ˜•ํƒœ๋กœ ๊ตฌํ˜„๋˜์–ด์žˆ์–ด NSCache๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

CoreLocation ์‚ฌ์šฉ

import CoreLocation

final class LocationSearchUseCase {
    
    func searchLocation(
        latitude: Double,
        longitude: Double,
        completion: @escaping (String?) -> Void
    ) {
        let findLocation = CLLocation(latitude: latitude, longitude: longitude)
        let geocoder = CLGeocoder()
        let locale = Locale(identifier: "en-US")
        
        geocoder.reverseGeocodeLocation(
                findLocation,
                preferredLocale: locale) { (place, error) in
                    let city = place?.last?.name
                    completion(city)
            }
    }
}

CoreLocation์˜ CLLocation์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ„๊ฒฝ๋„๋ฅผ ๋ฐ›์•„์™€ ํ˜„์žฌ ์œ„์น˜๋ฅผ ๊ฒฐ๊ณผ๋กœ ๋ฐ›์•„์™”์Šต๋‹ˆ๋‹ค.

์›น๋ทฐ ๊ตฌํ˜„ ๋ฐ ๊ตฌ๊ธ€ ์ง€๋„ ๋„์–ด์ฃผ๊ธฐ

private let webView: WKWebView = {
        let preferences = WKWebpagePreferences()
        preferences.allowsContentJavaScript = true
        let configuration = WKWebViewConfiguration()
        configuration.defaultWebpagePreferences = preferences
        let webView = WKWebView(frame: .zero, configuration: configuration)
        webView.translatesAutoresizingMaskIntoConstraints = false
        
        return webView
    }()

WebView๋ฅผ WKWebView๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋„์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ์œ„์˜ ์œ„๊ฒฝ๋„๋ฅผ ํ†ตํ•ด ๋ฐ›์€ ์ฃผ์†Œ๋ฅผ ํ†ตํ•ด ๊ตฌ๊ธ€์—์„œ ์ฃผ์†Œ๋ฅผ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ takeSnapShot ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด UIImage๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ด ์ด๋ฏธ์ง€๋ฅผ ์บ์‹ฑํ–ˆ์Šต๋‹ˆ๋‹ค.


ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

SearchController

SearchController์˜ SearchResultsUpdater = self ๋กœ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด ViewController์— ์ฑ„ํƒ์„ ํ•˜๋”๋ผ๋„ Delegate์ฒ˜๋Ÿผ ๋ฉ”์„œ๋“œ๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. SearchResultsController๋ฅผ ํ†ตํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด์ฃผ๋Š” ViewController๋ฅผ ๋”ฐ๋กœ ์ง€์ •ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค๋„ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

UseCase์˜ Import UIKit

final class ImageCacheData {
  
  let key: NSString
  let value: UIImage
  
  init(key: NSString, value: UIImage) {
      self.key = key
      self.value = value
  }
}

UseCase ๋‹จ์—์„œ UIKit์ด Import ๋˜์–ด์žˆ์œผ๋ฉด ์•ˆ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ ImageCacheData ๋ผ๋Š” ๊ฐ์ฒด๋กœ ์บ์‹ฑํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

์บ์‹ฑ์„ ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ KingFisher ์˜ ๊ตฌํ˜„์ฝ”๋“œ ์—์„œ๋„ ๊ฐ์ฒด ์ž์ฒด๋ฅผ ์บ์‹ฑํ•ด์ฃผ๋Š” ๊ฒƒ์„ ์ฐธ๊ณ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


Step1 PRํ›„ ๊ฐœ์„ ์‚ฌํ•ญ

Class Generic -> Method Generic

final class APIService {
       
   func request<T: Decodable>(
       _ type: RequestType,
       completion: @escaping (Result<T, Error>) -> Void
   ) {

ํ•˜๋‚˜์˜ APIService ์ธ์Šคํ„ด์Šค๋กœ ์—ฌ๋Ÿฌ๊ฐ€์ง€ Decodable ํƒ€์ž…์„ ๋””์ฝ”๋”ฉ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.


Step2 ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

SystemUnderTest์™€ setUpWithError/ tearDownWithError ๋ฉ”์„œ๋“œ๋กœ ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์„ฑ ๊ฐœ์„ 

๋งค๋ฒˆ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋ฉฐ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ๋ณด๋‹ค๋Š” ํŒŒ์ผ ๋ถ„๋ฆฌ์™€ sut์˜ ํ™œ์šฉ์œผ๋กœ

 var sut: DetailShowUseCase?

    override func setUpWithError() throws {
        let repository = DefaultWeatherRepository(service: MockAPIService())
        sut = DetailShowUseCase(weatherRepository: repository)
    }

    override func tearDownWithError() throws {
        sut = nil 
    }

๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ƒˆ๋กœ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ๊ณผ deinit์„ ํ•ด์ฃผ์–ด ํ…Œ์ŠคํŠธ๊ฐ€ ํŽธํ•ด์ง€๊ฒŒ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋น„๋™๊ธฐ ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ

 func test_AddressSearchUseCase_์ด๋งค๋™_์œ„๊ฒฝ๋„๋ฅผ_์ž…๋ ฅํ–ˆ์„๋•Œ_์ด๋งค๋™_์ฃผ์†Œ๊ฐ€_๋‚˜์˜จ๋‹ค() {
        let imaelatitude = 37.39508700000
        let imaelongitude = 127.12415500000
     
        let promise = expectation(description: "")
        self.sut?.searchLocation(latitude: imaelatitude, longitude: imaelongitude) { (address) in
            XCTAssertEqual(address!, "153-2 Imae-dong")
            promise.fulfill()
        }
        
        wait(for: [promise], timeout: 3)
    }

promised์™€ fulfill ๊ทธ๋ฆฌ๊ณ  wait ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ ๋ฉ”์„œ๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜์˜€์Šต๋‹ˆ๋‹ค.


Step2 PRํ›„ ๊ฐœ์„ ์‚ฌํ•ญ

๋„คํŠธ์›Œํฌ์™€ ๋ฌด๊ด€ํ•œ ํ…Œ์ŠคํŠธ

  var sut: DetailShowUseCase?

    override func setUpWithError() throws {
        let repository = DefaultWeatherRepository(service: APIService()

๋„คํŠธ์›Œํฌ์˜ ์ƒํ™ฉ์— ๋งž์ถฐ์„œ ํ…Œ์ŠคํŠธ ํ•˜๋˜ ๋ถ€๋ถ„์„

@testable import ChildOfWeather

final class MockAPIService: URLSessionNetworkService {
   
   func request<T>(decodedType: T.Type, requestType: RequestType, completion: @escaping (Result<T, APICallError>) -> Void) where T : Decodable {
       
       guard let mockObject = try? JSONDecoder().decode(T.self, from: Data())
       else {
           return completion(.failure(APICallError.failureDecoding))
       }
       
       completion(.success(mockObject))
   }
}

TestDouble์„ ์‚ฌ์šฉํ•˜์—ฌ ๋„คํŠธ์›Œํฌ์™€ ๋ฌด๊ด€ํ•œ ํ…Œ์ŠคํŠธ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

LocationManager ๊ณต์šฉ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

final class LocationManager {
    
    static let shared = LocationManager()
    let geocoder = CLGeocoder()
    let locationManager = CLLocationManager()
    
    private init() {
        
    }

๊ธฐ์กด Repository -> UseCase -> ViewModel ๊นŒ์ง€ ์ด์–ด์ง€๋˜ ํ๋ฆ„์œผ๋กœ CLGeocoder๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋Š”๋ฐ , ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ด€ํ•  ํ•„์š” ์—†๋‹ค๋Š” ์ ๊ณผ ์•ฑ ์ „์—ญ์ ์œผ๋กœ ๋งค๋ฒˆ ์“ฐ์ด๋Š” ๊ธฐ๋Šฅ์ด๋ผ ์ƒ๊ฐ์ด๋“ค์–ด CLGeocoder ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ๋น„์šฉ์ด ๋ฐ์ดํ„ฐ ์˜์—ญ์— ๊ณ„์† ๋‚จ์•„์žˆ๋Š” ์‹ฑ๊ธ€ํ†ค์˜ ๋น„์šฉ๋ณด๋‹ค ํฌ๋‹ค ์ƒ๊ฐ์ด ๋“ค์–ด LocationManager๋ผ๋Š” ๊ณต์šฉ ์ธ์Šคํ„ด์Šค๋ฅผ ๋‘๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ Dateformatter ์—ญ์‹œ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ๋น„์šฉ์ด ์ปค์„œ ๊ณต์šฉ ์ธ์Šคํ„ด์Šค๋กœ ๋งŒ๋“ค์–ด ์„ฑ๋Šฅ๊ณผ ํŽธ์˜์„ฑ์„ ๊ฐœ์„ ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.


Step3 RxSwift ์ ์šฉ

Reactive Extension ์œผ๋กœ ํ•„์š”ํ•œ ๋ถ€๋ถ„ ๊ตฌํ˜„

private extension Reactive where Base: UIImageView {
    func loadCacheView(webView: WKWebView) -> Binder<UIImage> {
        return Binder(self.base) { ImageView, image in
            webView.isHidden = true
            base.isHidden = false
            ImageView.image = image
        }
    }
}

RxCocoa๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ, ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ Reactive Extension์„ ํ†ตํ•ด ์›ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋‹ด์€ ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ณผ์ •์—์„œ ControlProperty์™€ ControlEvent ๊ทธ๋ฆฌ๊ณ  Binder์˜ ์ฐจ์ด์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

Input & Output ๋ชจ๋ธ๋ง

 struct Input {
        let viewWillAppear: Observable<Void>
        let capturedImage: Observable<ImageCacheData>
        let touchUpbackButton: Observable<Void>
    }
    
    struct Output {
        let selectedURLForMap: Observable<URLRequest?>
        let cachedImage: Observable<ImageCacheData>?
        let weatehrDescription: Observable<String>
        let capturedSuccess: Observable<Void>
        let dismiss: Observable<Void>
    }

์ด์ „ RxSwift ์—†์ด ๊ตฌํ˜„ํ–ˆ์„๋•Œ์™€๋Š” ๋‹ค๋ฅด๊ฒŒ Input ์ด๋ฒคํŠธ์™€ Output์˜ ์ •๋ณด๋ฅผ Nested Type์œผ๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ์ฝ”๋“œ์˜ ์ง๊ด€์„ฑ์„ ๊ฐœ์„ ํ•˜๊ณ  ํ•˜๋‚˜์˜ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ViewController์™€ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ ์šฉ

 func search(name: String?) -> Observable<[City]> {
        
        guard let name = name, name != ""
        else {
            self.fetchCityList()
            return self.assetData.asObservable()
        }
        let filteredCity = assetData.value.filter { $0.name.hasPrefix(name) }
        
        return Observable<[City]>.just(filteredCity)
    }

์›๋ณธ ๋ฐ์ดํ„ฐ๋Š” ์œ ์ง€ํ•œ์ƒํƒœ๋กœ SearchBar์˜ Search๋ฅผ ๋ฆฌํ„ด๊ฐ’์ด ์žˆ๋Š” ์ƒํƒœ๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ๋ช‡๋ฒˆ์„ ์‹œ๋„ํ•ด๋„ ๊ฐ’์ด ๋ณ€ํ•˜์ง€ ์•Š๊ฒŒ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.


Step3 PRํ›„ ๊ฐœ์„ ์‚ฌํ•ญ

Input์—์„œ๋ถ€ํ„ฐ ์ด์–ด์ง€๋Š” ์ŠคํŠธ๋ฆผ

  let capturedSuccess = input.capturedImage
            .withUnretained(self)
            .filter { _ in
                self.imageCacheUseCase.hasCacheExist(cityName: self.extractCity().name) == false }
            .do(onNext: { (self, image) in
                self.imageCacheUseCase.setCache(object: image)
            }).map { _ in }

Output์œผ๋กœ ๋ณด๋‚ด์ฃผ๋Š” ์ŠคํŠธ๋ฆผ์ด Input์˜ ์ด๋ฒคํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด์„œ ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ -> ์ฒ˜๋ฆฌ -> ๊ตฌ๋… ์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ๋ฆ„์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ๊ตฌ๋…์€ ViewController์—์„œ ์‹คํ–‰

  let dismiss = input.touchUpbackButton
      .withUnretained(self)
      .observe(on: MainScheduler.instance)
      .do(onNext: { _ in
          self.coordinator.occuredViewEvent(with: .dismissDetailShowUIViewController)
      }).map { _ in }

do(onNext:) ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ๊ตฌ๋…์ด ๋˜์—ˆ์„ ๋•Œ ์‹คํ–‰ํ•  ๋ฉ”์„œ๋“œ๋งŒ ๋‚ด๋ถ€์— ์ •์˜ํ•˜๊ณ  ๋ชจ๋“  ๊ตฌ๋…์€ ViewController๊ฐ€ ํ•˜๊ฒŒ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ์„  ํ›„ ๋‹จ๋ฐฉํ–ฅ์œผ๋กœ ๋ชจ๋“  ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐ”์ธ๋”ฉ ๋˜์–ด ์กฐ๊ธˆ ๋” RxSwift์Šค๋Ÿฝ๊ฒŒ ์ฝ”๋“œ๊ฐ€ ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


Step4 RxNimble์„ ํ†ตํ•œ ViewModel ํ…Œ์ŠคํŠธ

๊ฐ ViewModel ๋ณ„๋กœ setUp ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ๋…๋ฆฝ์  ํ…Œ์ŠคํŠธ

override func setUp() {
        self.schduler = TestScheduler(initialClock: 0)
        self.disposeBag = DisposeBag()
        self.capturedPublish = PublishSubject<ImageCacheData>()
        self.viewWillAppearPusblish = PublishSubject<Void>()
        self.touchUpbackButtonPublish = PublishSubject<Void>()
        self.schduler = TestScheduler(initialClock: 0)
        self.viewModel = DetailWeatherViewModel(
            detailShowUseCase: DetailWeatherFetchUseCase(weatherRepository: DefaultWeatherRepository(service: MockAPIService())),
            imageCacheUseCase: ImageCacheUseCase(imageProvideRepository: DefaultImageProvideRepository()),
            coodinator: SearchViewCoordinator(
                imageCacheUseCase: ImageCacheUseCase(imageProvideRepository: DefaultImageProvideRepository())),
                city: City.EMPTY
            )
            
        self.output = viewModel.transform(input: .init(viewWillAppear: self.viewWillAppearPusblish.asObserver(), capturedImage: self.capturedPublish.asObserver(), touchUpbackButton: self.touchUpbackButtonPublish.asObserver()))
    }

Void ๋น„๊ต๋ฅผ ์œ„ํ•œ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

public func equal<Void>(_ expectedValue: Void?) -> Predicate<Void> {
    return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
        let actualValue = try actualExpression.evaluate()
        switch (expectedValue, actualValue) {
        case (nil, _?):
            return PredicateResult(status: .fail, message: msg.appendedBeNilHint())
        case (nil, nil), (_, nil):
            return PredicateResult(status: .fail, message: msg)
        default:
            var isEqual = false

            if String(describing: expectedValue).count != 0, String(describing: expectedValue) == String(describing: actualValue) {
                isEqual = true
            }
            return PredicateResult(bool: isEqual, message: msg)
        }
    }

๊ฐ ์ƒํ™ฉ๋ณ„๋กœ Testํ•˜์—ฌ ViewModel์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ

func testCapturedImage() {
        let imageCachedData = ImageCacheData(key: "123", value: UIImage())
        schduler.createColdObservable([
            .next(3, imageCachedData)
        ]).bind(to: self.capturedPublish).disposed(by: self.disposeBag)
        
        expect(self.output.cachedImage).events(scheduler: scheduler, disposeBag: self.disposeBag).to(equal([
            .next(4, imageCachedData)
        ]))
    }
 func testBackButtonRunDismiss() {
        scheduler.createColdObservable([
            .next(5, ())
        ]).bind(to: self.touchUpbackButtonPublish).disposed(by: self.disposeBag)
        
        expect(self.output.dismiss).events(scheduler: self.scheduler, disposeBag: self.disposeBag).to(equal([
            .next(5, ())
        ]))
    }

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

<์ฒซ๋ฒˆ์งธ ๋ฌธ์ œ: Domain Layer์™€ ViewModel์—์„œ UIkit ์„ import ํ•˜๋˜ ๋ฌธ์ œ >

๋ฌธ์ œ ์ƒํ™ฉ

  • ํ•œ๋ฒˆ ๋“ค์–ด๊ฐ„ Cell์˜ DetailView์˜ ํ™”๋ฉด์„ ๋งค๋ฒˆ WebView๋กœ ๋„์–ด์ฃผ๊ฒŒ ๋œ๋‹ค๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ข‹์ง€ ์•Š๊ณ  ๋งค๋ฒˆ URLRequest๋ฅผ ํ†ตํ•ด ๋ถˆ๋Ÿฌ์™€์•ผํ•ด์„œ ์„ฑ๋Šฅ์ƒ์˜ ๋ถˆ์ด์ต์ด ์žˆ์Šต๋‹ˆ๋‹ค. WebView์˜ ๊ธฐ๋Šฅ ์ค‘ โ€˜takeSnapshotโ€™ ์ด๋ผ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ๋Š” ๊ฒƒ์„ ๊ณต์‹๋ฌธ์„œ๋ฅผ ํ†ตํ•ด ์ฐพ์•„๋ณด๊ณ  UIImage๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์บ์‹ฑ์„ ํ•ด์ฃผ๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • Dataformatter ์ฒ˜๋Ÿผ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ๋น„์šฉ์ด ์ปค Singleton์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ์— ๊ณ„์† ๋‚จ์•„์žˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ๋” ์œ ๋ฆฌํ•œ ์ƒํ™ฉ๋„ ์•„๋‹ˆ์—ฌ์„œ Data Layer์— ์žˆ๋Š” Repository์— NSCache๋ฅผ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • DataLayer โ†’ DomainLayer (UseCase) โ†’ ViewModel ๋ชจ๋‘ UIImage๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด ๋ชจ๋“  Layer๊ฐ€ import UIkit์„ ํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์ด ์ƒ๊ฒผ๊ณ  ์ด๋Š” ํด๋ฆฐ์•„ํ‚คํ…์ณ๋ฅผ ์œ„๋ฐ˜ ํ•  ๋ฟ ์•„๋‹ˆ๋ผ ํ…Œ์ŠคํŠธ ์‹œ ๋ถˆ๋ฆฌํ•˜๋‹ค๋Š” ํŒ๋‹จ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ ํ•ด๊ฒฐ ๊ณผ์ •

  1. UIImage์˜ Data๋งŒ ๋ฐ›์•„์„œ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ๋ฒ•

    ์ฒ˜์Œ ์ƒ๊ฐํ–ˆ๋˜ ๋ฐฉ๋ฒ•์€ UIImage์˜ ๋ฐ์ดํ„ฐ๋งŒ ViewModel์—์„œ ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ๋ฒ•์ด์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์บ์‹ฑ์ด ์ œ๋Œ€๋กœ ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๊ณ , ViewController์—์„œ ๊ฒฐ๊ตญ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•ด์„œ UIImage๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ํด๋ฆฐ์•„ํ‚คํ…์ณ ๊ทธ๋ฆฌ๊ณ  MVVM์—์„œ ViewController๋Š” ์ตœ๋Œ€ํ•œ ์ˆ˜๋™์ ์ธ ์—ญํ• ์„ ํ•ด์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ธฐ์— WebView์ฒ˜๋Ÿผ ๋ถ€๋“์ดํ•˜๊ฒŒ load๋ฅผ ViewController์—์„œ ํ•ด์ฃผ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ฉด ์ง€ํ–ฅํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ํ•˜๋‚˜์˜ ํƒ€์ž…์œผ๋กœ ๋ฌถ๋Š” ๋ฐฉ๋ฒ•

    ๊ทธ ๋‹ค์Œ์œผ๋กœ ๋– ์˜ฌ๋ฆฐ ๋ฐฉ๋ฒ•์€ ํ˜„์—…์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ Kingfisher๋ฅผ ๋ณด๊ณ  ์ฐธ๊ณ ํ•˜์˜€๋Š”๋ฐ, ํ•˜๋‚˜์˜ ํƒ€์ž… ๋‚ด๋ถ€์— Key, Value ์Œ์„ ๋‘์–ด Value ๊ฐ’์— UIImage๋ฅผ ๊ฐ€์ง€๊ฒŒ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ํƒ€์ž…์€ Domain Layer์˜ Model๋กœ ๋‘์–ด ํด๋ฆฐ์•„ํ‚คํ…์ณ์— ์œ„๋ฐ˜๋˜์ง€๋„ ์•Š์€ ๋ฟ๋”๋Ÿฌ ์บ์‹ฑ์ด ์ž˜ ์ž‘๋™ํ•˜๋Š” ์ง€ ์—ญ์‹œ๋„ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ํ™•์ธ ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ Data๋งŒ ๋ฐ›์•„์„œ ์ „๋‹ฌํ•˜๋˜ ์ด์ „๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ ViewModel์ด ๋ณด๋‚ธ ํƒ€์ž…์˜ value๋ฅผ ViewController ๋‚ด๋ถ€์—์„œ ๊บผ๋‚ด์“ฐ๊ธฐ๋งŒ ํ•˜๋ฉด ๋˜์„œ ๋ณ€ํ™˜ํ•˜์ง€ ์•Š๊ณ  ์ตœ๋Œ€ํ•œ ์ˆ˜๋™์ ์ด๊ฒŒ ViewController๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๊ณ  ์ดํ•ดํ•˜๋Š” ์—ฐ์Šต์„ ํ•ด๋‘” ์ด์ „๊นŒ์ง€์˜ ๊ฒฝํ—˜์ด ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ๋น ๋ฅด๊ฒŒ ํ•ด๊ฒฐ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ์—ˆ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์‚ฌ์šฉ์ž๋Š” WebView๊ฐ€ ๋กœ๋“œ๋˜๋Š” ์‹œ๊ฐ„์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ์ž์ฃผ ๋“ค์–ด๊ฐ€๋Š” Cell์˜ ์ง€๋„๋ฅผ UIImage๋กœ ๋ฐ”๋กœ ๋ฐ›์•„ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

< ๋‘๋ฒˆ์งธ ๋ฌธ์ œ: ์ด๋ฏธ์ง€๋ฅผ ์บ์‹ฑํ•œ ํ›„ WebView๋ฅผ UIImage๋กœ ๋Œ€์ฒดํ•˜๋Š” ๋ฌธ์ œ>

๋ฌธ์ œ ์ƒํ™ฉ

  • ViewModel์˜ Input & Output Modeling ์ค‘ Output์€ Relay๋ฉด ์•ˆ๋œ๋‹ค๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ทฐ๋ฅผ ๊ทธ๋ฆฌ๋Š” ์ž‘์—…์ด๋‹ˆ ๋Š์ด์ง€ ์•Š๋Š” ์ŠคํŠธ๋ฆผ์ธ Relay์—ฌ๋„ ๋˜๋Š” ๊ฒƒ ์ด๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“ค์—ˆ์ง€๋งŒ ๊ทธ๋ ‡๋‹ค๋ฉด ViewController๋Š” ViewModel์—์„œ ์˜จ Relay์— ์ด๋ฒคํŠธ๊ฐ€ ์ด๋‹Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ ‘๊ทผ์€ MVVM๊ณผ๋Š” ์–ด์šธ๋ฆฌ์ง€ ์•Š์„ ๋ฟ๋”๋Ÿฌ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋‹ต์ง€ ์•Š์€ ์ ‘๊ทผ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์œ„์˜ ์ด์œ ๋กœ ViewModel์˜ Output์„ Observable๋กœ ๋งŒ๋“ค์–ด ์ „๋‹ฌ ํ•œ ํ›„ ViewController๊ฐ€ asDriver ์˜คํผ๋ ˆ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ์ŠคํŠธ๋ฆผ์„ Driver๋กœ ๋ณ€๊ฒฝ ํ•œ ํ›„ drive๋ฅผ ํ†ตํ•ด UIComponents์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—์„œ ์˜คํผ๋ ˆ์ดํ„ฐ drive๋Š” ํ•˜๋‚˜์˜ ์ผ ๋ฐ–์— ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ  drive(onNext:) ํด๋กœ์ €๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ˆœํ™˜์ฐธ์กฐ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์–ด์„œ ์ตœ๋Œ€ํ•œ ํด๋กœ์ €๋ฅผ ์ง€ํ–ฅํ•˜๋ฉฐ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ์ŠคํŠธ๋ฆผ์—์„œ WebView๋ฅผ hide์‹œ์ผœ์ฃผ๋Š” ์ž‘์—…๊ณผ hide๋œ ์ƒํƒœ์ธ UIImageView๋ฅผ ํ™”๋ฉด์— ๋„์–ด์ฃผ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

    ๋‘๋ฒˆ subscribeํ•˜๋ฉด ๋˜์ง€๋งŒ, ์ด๋ ‡๊ฒŒ ๋œ๋‹ค๋ฉด ๋‘๊ฐœ์˜ ์ŠคํŠธ๋ฆผ์ด ์ƒ๊ธฐ๊ณ  ์บ์‹ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ณผ์ •์ด 2๋ฒˆ ์ผ์–ด๋‚˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ฌธ์ œ ํ•ด๊ฒฐ ๊ณผ์ •

  1. ViewModel์—์„œ Share Operator๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋‘๋ฒˆ ๊ตฌ๋…ํ•˜๋Š” ๋ฐฉ๋ฒ•

    ๊ฐ€์žฅ ๋จผ์ € ๋– ์˜ฌ๋ฆฐ ๋ฐฉ๋ฒ•์€ 2๋ฒˆ์˜ ๊ตฌ๋…์œผ๋กœ ์ธํ•œ 2๊ฐœ์˜ ์ŠคํŠธ๋ฆผ์ด ์ƒ๊ธฐ๋Š” ๊ฒƒ์„ ๋ง‰์•„์ฃผ๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋งค๋ฒˆ ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์œผ๋กœ ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ์ถ”ํ›„์— ๋” ํฐ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งก๊ฑฐ๋‚˜ ์ง„ํ–‰ํ•˜๊ฒŒ ๋  ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ์ถ”์ ํ•˜๋Š” ๊ฒƒ์ด ์–ด๋ ต๋‹ค๊ณ  ์ƒ๊ฐ์ด ๋“ค์–ด ๋‹ค๋ฅธ ๋ฐฉํ–ฅ์œผ๋กœ ์ƒ๊ฐ์„ ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  2. Reactive Extension ํ™œ์šฉ

    RxSwift, RxCocoa๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ControlProperty, ControlEvent ๊ทธ๋ฆฌ๊ณ  Binder์˜ ์ฐจ์ด๋ฅผ ๋ชจ๋ฅธ๋‹ค๋ฉด ์•ˆ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์ดํ•ดํ•˜๊ณ  ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๊ฐœ๋ฐœ์ž ๋ณธ์ธ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์กด์„ฑ์ด ์ค„์–ด๋“œ๋Š” ์ผ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์˜€๊ณ  ์ด๋Ÿฌํ•œ ์ƒ๊ฐ์„ ๊ธฐ๋ฐ˜์œผ๋กœ Reactive Extension์„ ํ†ตํ•ด ์›ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

WebView๋ฅผ hidden ์ƒํƒœ๋กœ, ImageView์˜ Image์— ViewModel์—์„œ ์˜ค๋Š” ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ์–ด์ฃผ๊ณ  ํ™”๋ฉด์— ํ‘œ์‹œํ•ด์ฃผ๋Š” ์ปค์Šคํ…€ ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ํ•œ๋ฒˆ์˜ ๊ตฌ๋…์œผ๋กœ ๋ชจ๋“  ์ž‘์—…์ด ์ผ์–ด๋‚˜๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ViewWillAppear ์ด๋ฒคํŠธ๋‚˜ ViewWillDisappear ์ด๋ฒคํŠธ ์—ญ์‹œ๋„ ์Šค์Šค๋กœ ControlEvent๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ํ–ฅ์ƒ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.

<์„ธ๋ฒˆ์งธ ๋ฌธ์ œ: ์ดˆ๋‹น ํšŸ์ˆ˜์ œํ•œ์ด ๊ฑธ๋ฆฐ API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ TableView๋ฅผ ๋„์–ด์ฃผ๋Š” ๋ฌธ์ œ>

๋ฌธ์ œ์ƒํ™ฉ

  • API๋ฅผ ํ†ตํ•ด ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ํ•˜์—ฌ 100๊ฐœ์˜ TableView๋ฅผ ๋„์–ด์ฃผ์–ด์•ผ ํ•˜๋Š”๋ฐ ์ดˆ๋‹น 10๊ฑด ์ œํ•œ์ด ๊ฑธ๋ ค์žˆ์–ด ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ์ค‘์ผ๋•Œ ํ™”๋ฉด์ด ํ‘œํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
  • ์ปค์Šคํ…€ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด์„œ Semaphore Signal๊ณผ wait์„ ์ด์šฉํ•ด ์Šค์ผ€์ค„๋ง์„ ํ•ด์ฃผ์—ˆ์ง€๋งŒ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.
  • Cell์˜ ์ด๋ฏธ์ง€๋งŒ ๋Šฆ๊ฒŒ ๋กœ๋“œ๋˜๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด cell์—๊ฒŒ Observable๋ฅผ ์ „๋‹ฌํ•ด์ฃผ๊ณ  Cell์—์„œ ๊ตฌ๋…์„ ํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„์˜ค๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ ํ•ด๊ฒฐ ๊ณผ์ •

  • ์ดˆ๋‹น 10๊ฑด ์ œํ•œ์ด๋ผ 100๊ฐœ๋ฅผ ๋„์šฐ๋ ค๋ฉด ์ด 10์ดˆ๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ํ•˜๋Š” Observable์— retry Operator๋ฅผ ํ†ตํ•ด์„œ flatMap์œผ๋กœ Observable ํ˜•์ธ Timer๋กœ ๋ฐ”๊ฟ”์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํƒ€์ด๋จธ์˜ period๋ฅผ 0.1์ดˆ๋กœ ๋‘์–ด ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฏธ์ง€๊ฐ€ ๋’ค์„ž์ด๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ DispatchQueue๋ฅผ ์‚ฌ์šฉํ•œ IndexPath ๊ฒ€์ฆ์œผ๋กœ ํ•ด๋‹นํ•˜๋Š” index์ผ ๋•Œ๋งŒ Cell์— content๋ฅผ ์ฃผ๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • Cell์˜ PrepareForReuse ์•ˆ์—์„œ dispose๋ฅผ ํ•ด์ฃผ์–ด ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ์ค‘์ธ๋ฐ ์Šคํฌ๋กค์„ ๋‚ด๋ ค์„œ ๋” ์ด์ƒ Observableํ•œ Data๋ฅผ ๋ฐ›์•„์˜ค์ง€ ์•Š์•„๋„ ๋  ๊ฒฝ์šฐ์— ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๊ฐœ์„ ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.