- ๋จ์: ๊ธฐ๋ฅ ๋จ์
- ์ปค๋ฐ ์คํ์ผ: ์นด๋ฅด๋ง ์คํ์ผ
- Step1
์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด ๋ชจ๋ ๊ธฐ๋ฅ ๊ตฌํ
๋ชฉํ: ์ด์ ๊น์ง ์งํํ๋ ํ๋ก์ ํธ์๋ ๋ค๋ฅด๊ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ด ๊ตฌํํ์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์กด์ฑ์ ๋ฎ์ถ๋ค
- Step2
ํ ์คํธ ์ฝ๋ ๊ตฌํ
- Step3
RxSwift๋ฅผ importํ์ฌ ๋จ๋ฐฉํฅ ๋ฐ์ธ๋ฉ
๋ชฉํ: RxSwift์ RxCocoa ์ฌ์ฉ, ํด๋ฆฐ์ํคํ ์ณ ๊ท์น ์ค์
- Step4
AppCoordinator๋ฅผ ์ฌ์ฉํด ์์กด์ฑ ์ฃผ์ ๋ฐ TabBarController ๋ฅผ ํตํ ํ๋ฉด์ ํ
Compositional Layout์ ์ฌ์ฉํ CollectionView ๋ฐ DiffableDataSourceํ์ฉ
RxNimble์ ํ์ฉํ ViewModel, UseCase ํ ์คํธ
RxSwift, Clean Architecture MVVM, RxCocoa, Swift Package Manager
Coordinator, WebView, Localization
| ๋์ ๊ฒ์ | ๊ฒ์์ฐฝ๋ ธ์ถ |
|---|---|
![]() |
![]() |
| ๋ ์จ ์ ๋ณด | ํ์ฌ ์์น ํ์ |
|---|---|
![]() |
![]() |
final class APIService {
func request<T: Decodable>(
_ type: RequestType,
completion: @escaping (Result<T, Error>) -> Void
) {๋ค๋ฅธ URL๋ก ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ์ํฉ์ ๋๋นํ์ฌ Generic์ผ๋ก ๊ตฌํํ์ต๋๋ค.
protocol DetailViewModelDelegate: AnyObject {
func loadWebView(url: URL)
func loadTodayDescription(weather description: String)
func loadImageView()
func cacheImage()
}ViewModel์ ViewController๋ฅผ ์์ง ๋ชปํ๊ธฐ์ Delegate ํจํด์ ์ฌ์ฉํ์์ต๋๋ค.
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๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
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์ SearchResultsUpdater = self ๋ก ํด์ฃผ์ง ์์ผ๋ฉด ViewController์ ์ฑํ์ ํ๋๋ผ๋ Delegate์ฒ๋ผ ๋ฉ์๋๊ฐ ์ ์ฉ๋์ง ์์์ต๋๋ค. SearchResultsController๋ฅผ ํตํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ๋ํ๋ด์ฃผ๋ ViewController๋ฅผ ๋ฐ๋ก ์ง์ ํด ์ค ์ ์๋ค๋ ์ฌ์ค๋ ์๊ฒ ๋์์ต๋๋ค.
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 ์ ๊ตฌํ์ฝ๋ ์์๋ ๊ฐ์ฒด ์์ฒด๋ฅผ ์บ์ฑํด์ฃผ๋ ๊ฒ์ ์ฐธ๊ณ ํ์์ต๋๋ค.
final class APIService {
func request<T: Decodable>(
_ type: RequestType,
completion: @escaping (Result<T, Error>) -> Void
) {ํ๋์ APIService ์ธ์คํด์ค๋ก ์ฌ๋ฌ๊ฐ์ง Decodable ํ์ ์ ๋์ฝ๋ฉ ํ ์ ์๊ฒ ๋ณ๊ฒฝํ์์ต๋๋ค.
๋งค๋ฒ ์ธ์คํด์ค๋ฅผ ์์ฑํด์ฃผ๋ฉฐ ํ ์คํธํ๊ธฐ ๋ณด๋ค๋ ํ์ผ ๋ถ๋ฆฌ์ 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 ์ ์ฌ์ฉํ์ฌ ๋น๋๊ธฐ ๋ฉ์๋๋ฅผ ํ ์คํธํ์์ต๋๋ค.
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์ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ์ ๋ฌด๊ดํ ํ ์คํธ๋ก ๋ง๋ค์์ต๋๋ค.
final class LocationManager {
static let shared = LocationManager()
let geocoder = CLGeocoder()
let locationManager = CLLocationManager()
private init() {
}
๊ธฐ์กด Repository -> UseCase -> ViewModel ๊น์ง ์ด์ด์ง๋ ํ๋ฆ์ผ๋ก CLGeocoder๋ฅผ ๊ตฌํํ์๋๋ฐ , ๋ฐ์ดํฐ๋ฅผ ๋ณด๊ดํ ํ์ ์๋ค๋ ์ ๊ณผ ์ฑ ์ ์ญ์ ์ผ๋ก ๋งค๋ฒ ์ฐ์ด๋ ๊ธฐ๋ฅ์ด๋ผ ์๊ฐ์ด๋ค์ด CLGeocoder ์ธ์คํด์ค ์์ฑ๋น์ฉ์ด ๋ฐ์ดํฐ ์์ญ์ ๊ณ์ ๋จ์์๋ ์ฑ๊ธํค์ ๋น์ฉ๋ณด๋ค ํฌ๋ค ์๊ฐ์ด ๋ค์ด LocationManager๋ผ๋ ๊ณต์ฉ ์ธ์คํด์ค๋ฅผ ๋๊ฒ ๋์์ต๋๋ค. ์ด์ ๋ง์ฐฌ๊ฐ์ง๋ก Dateformatter ์ญ์ ์ธ์คํด์ค ์์ฑ ๋น์ฉ์ด ์ปค์ ๊ณต์ฉ ์ธ์คํด์ค๋ก ๋ง๋ค์ด ์ฑ๋ฅ๊ณผ ํธ์์ฑ์ ๊ฐ์ ์์ผฐ์ต๋๋ค.
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์ ์ฐจ์ด์ ๋ํด ๊ณ ๋ฏผํด๋ณด์์ต๋๋ค.
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๋ฅผ ๋ฆฌํด๊ฐ์ด ์๋ ์ํ๋ก ๊ตฌํํ์ฌ ๋ช๋ฒ์ ์๋ํด๋ ๊ฐ์ด ๋ณํ์ง ์๊ฒ ๊ตฌํํ์ต๋๋ค.
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์ ์ด๋ฒคํธ๋ถํฐ ์์ํ๊ฒ ํ์์ต๋๋ค. ์ด๋ฅผ ํตํด์ ์ฌ์ฉ์ ์ด๋ฒคํธ -> ์ฒ๋ฆฌ -> ๊ตฌ๋ ์ ์์ฐ์ค๋ฌ์ด ํ๋ฆ์ ๋ง๋ค์์ต๋๋ค.
let dismiss = input.touchUpbackButton
.withUnretained(self)
.observe(on: MainScheduler.instance)
.do(onNext: { _ in
self.coordinator.occuredViewEvent(with: .dismissDetailShowUIViewController)
}).map { _ in }do(onNext:) ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ, ๊ตฌ๋ ์ด ๋์์ ๋ ์คํํ ๋ฉ์๋๋ง ๋ด๋ถ์ ์ ์ํ๊ณ ๋ชจ๋ ๊ตฌ๋ ์ ViewController๊ฐ ํ๊ฒ ๊ตฌํํ์ต๋๋ค. ๊ฐ์ ํ ๋จ๋ฐฉํฅ์ผ๋ก ๋ชจ๋ ๋ฉ์๋๊ฐ ๋ฐ์ธ๋ฉ ๋์ด ์กฐ๊ธ ๋ RxSwift์ค๋ฝ๊ฒ ์ฝ๋๊ฐ ๊ฐ์ ๋์์ต๋๋ค.
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()))
}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)
}
}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, ())
]))
}- ํ๋ฒ ๋ค์ด๊ฐ Cell์ DetailView์ ํ๋ฉด์ ๋งค๋ฒ WebView๋ก ๋์ด์ฃผ๊ฒ ๋๋ค๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ์ข์ง ์๊ณ ๋งค๋ฒ URLRequest๋ฅผ ํตํด ๋ถ๋ฌ์์ผํด์ ์ฑ๋ฅ์์ ๋ถ์ด์ต์ด ์์ต๋๋ค. WebView์ ๊ธฐ๋ฅ ์ค โtakeSnapshotโ ์ด๋ผ๋ ๊ธฐ๋ฅ์ด ์๋ ๊ฒ์ ๊ณต์๋ฌธ์๋ฅผ ํตํด ์ฐพ์๋ณด๊ณ UIImage๋ก ๋ณํํ์ฌ ์บ์ฑ์ ํด์ฃผ๋ ค๊ณ ํ์ต๋๋ค.
- Dataformatter ์ฒ๋ผ ์ธ์คํด์ค ์์ฑ ๋น์ฉ์ด ์ปค Singleton์ผ๋ก ๋ฉ๋ชจ๋ฆฌ์ ๊ณ์ ๋จ์์๊ฒ ํ๋ ๊ฒ์ด ๋ ์ ๋ฆฌํ ์ํฉ๋ ์๋์ฌ์ Data Layer์ ์๋ Repository์ NSCache๋ฅผ ์ฌ์ฉ ํ ์ ์๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์์ต๋๋ค.
- DataLayer โ DomainLayer (UseCase) โ ViewModel ๋ชจ๋ UIImage๋ฅผ ๊ฐ์ง๊ณ ์์ด ๋ชจ๋ Layer๊ฐ import UIkit์ ํด์ผํ๋ ์ํฉ์ด ์๊ฒผ๊ณ ์ด๋ ํด๋ฆฐ์ํคํ ์ณ๋ฅผ ์๋ฐ ํ ๋ฟ ์๋๋ผ ํ ์คํธ ์ ๋ถ๋ฆฌํ๋ค๋ ํ๋จ์ ํ์์ต๋๋ค.
-
UIImage์ Data๋ง ๋ฐ์์ ์ ๋ฌํ๋ ๋ฐฉ๋ฒ
์ฒ์ ์๊ฐํ๋ ๋ฐฉ๋ฒ์ UIImage์ ๋ฐ์ดํฐ๋ง ViewModel์์ ๋ฐ์์ค๋ ๋ฐฉ๋ฒ์ด์์ต๋๋ค.
ํ์ง๋ง ์บ์ฑ์ด ์ ๋๋ก ๋์ง ์๋ ๋ฌธ์ ๊ฐ ์๊ฒผ๊ณ , ViewController์์ ๊ฒฐ๊ตญ ์ด ๋ฐ์ดํฐ๋ฅผ ๋ณํํด์ UIImage๋ก ์ฌ์ฉํด์ผ ํ์ต๋๋ค. ํด๋ฆฐ์ํคํ ์ณ ๊ทธ๋ฆฌ๊ณ MVVM์์ ViewController๋ ์ต๋ํ ์๋์ ์ธ ์ญํ ์ ํด์ผํ๋ค๊ณ ์๊ฐํ๊ธฐ์ WebView์ฒ๋ผ ๋ถ๋์ดํ๊ฒ load๋ฅผ ViewController์์ ํด์ฃผ๋ ๊ฒฝ์ฐ๊ฐ ์๋๋ฉด ์งํฅํด์ผ ํ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
-
ํ๋์ ํ์ ์ผ๋ก ๋ฌถ๋ ๋ฐฉ๋ฒ
๊ทธ ๋ค์์ผ๋ก ๋ ์ฌ๋ฆฐ ๋ฐฉ๋ฒ์ ํ์ ์์ ์์ฃผ ์ฌ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ Kingfisher๋ฅผ ๋ณด๊ณ ์ฐธ๊ณ ํ์๋๋ฐ, ํ๋์ ํ์ ๋ด๋ถ์ Key, Value ์์ ๋์ด Value ๊ฐ์ UIImage๋ฅผ ๊ฐ์ง๊ฒ ํ๋ ๋ฐฉ๋ฒ์ด์์ต๋๋ค. ์ด ํ์ ์ Domain Layer์ Model๋ก ๋์ด ํด๋ฆฐ์ํคํ ์ณ์ ์๋ฐ๋์ง๋ ์์ ๋ฟ๋๋ฌ ์บ์ฑ์ด ์ ์๋ํ๋ ์ง ์ญ์๋ ํ ์คํธ๋ฅผ ํตํด ํ์ธ ํ ์ ์์์ต๋๋ค.
๋ํ Data๋ง ๋ฐ์์ ์ ๋ฌํ๋ ์ด์ ๊ณผ๋ ๋ค๋ฅด๊ฒ ViewModel์ด ๋ณด๋ธ ํ์ ์ value๋ฅผ ViewController ๋ด๋ถ์์ ๊บผ๋ด์ฐ๊ธฐ๋ง ํ๋ฉด ๋์ ๋ณํํ์ง ์๊ณ ์ต๋ํ ์๋์ ์ด๊ฒ ViewController๋ฅผ ์ ์งํ ์ ์์์ต๋๋ค.
๋ค๋ฅธ ์ฌ๋์ ์ฝ๋๋ฅผ ๋ณด๊ณ ์ดํดํ๋ ์ฐ์ต์ ํด๋ ์ด์ ๊น์ง์ ๊ฒฝํ์ด ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ๋น ๋ฅด๊ฒ ํด๊ฒฐ ํ ์ ์๊ฒ ํด์ฃผ์๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ด๋ฌํ ๋ฐฉ๋ฒ์ผ๋ก ์ฌ์ฉ์๋ WebView๊ฐ ๋ก๋๋๋ ์๊ฐ์ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์์ฃผ ๋ค์ด๊ฐ๋ Cell์ ์ง๋๋ฅผ 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๋ฒ ์ผ์ด๋๊ฒ ๋ฉ๋๋ค.
-
ViewModel์์ Share Operator๋ฅผ ์ฌ์ฉํ๊ณ ๋๋ฒ ๊ตฌ๋ ํ๋ ๋ฐฉ๋ฒ
๊ฐ์ฅ ๋จผ์ ๋ ์ฌ๋ฆฐ ๋ฐฉ๋ฒ์ 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๋ฅผ ๋ฐ์์ค์ง ์์๋ ๋ ๊ฒฝ์ฐ์ ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๊ฐ์ ์์ผฐ์ต๋๋ค.



