iosswift 2022. 9. 26. 10:47

Server로 부터 날짜를 가져오고, 날짜를 화면에 띄워주는 서비스를 생각해보자.

 

 

먼저 Repository 에서 Server 날짜를 가져온다. 

가져오는 Server 의 데이터 (Entity)는 우리가 원하는 데이터 자체가 아니다.

즉, 가공이 필요하다. 

 

따라서 그 다음으로 Service 에서 Repository 에 있는

Entity 를 가져와 이를 우리가 사용할 데이터 형태(Model)로 가공한다.  

 

그 후에는 가공된 데이터를 화면에 보여주어야 하는데,

 Service 에는 'Date' Type 이 사용되지만 화면에서는 String Type이 필요하므로 데이터 형태의 변환이 또다시 필요하다.

 

이때, 그 가공을 맡는 곳을 ViewModel 이라고 한다.

 

그 후 View 에서 ViewModel 의 데이터를 가져와 화면에 보여주면 일련의 프로세스가 완료된다. 

 

여기서, Model 은  Entity, Model, ViewModel 로 총 3개이다.

각 Model 의 사용 목적은 같으나, 사용처(Repo, Service, VM) 는 다르다. 

 

 

이 중 비즈니스 로직은 Service 에서 처리하며, 가장 중요한 부분이다.

 

데이터는 위에서 아래로 이동하며 가공되게 되는 반면, 

의존성은 아래에서 위 방향을 향한다.

 

의존성에 대해 쉽게 말하면, View 는 ViewModel 데이터를 사용하고, ViewModel 은 View 에 대해서 알 수 없는 관계이다. 즉 말그대로 View 가 ViewModel 에 '의존' 한다.

 

이때, ViewModel 의 값을 View 에 실시간으로 보여주기 위해서는 ViewModel 의 값을 모니터링 해야하는데, 이때 여러 스타일의 Data Binding 이 사용될 수 있다.

asyncronous

asynchronous

그 중 가장 많이 쓰이는 Binding 방식이 RxSwift 를 이용한 방식이다. RxSwift 가 Data Binding 에 유리한 몇가지 특징 (asynchronous process in a stream way 등) 을 가지고 있으나,  반드시 Binding 에 이것만 쓰여야 하는 것은 아니다. 

 

 

위와 같은 흐름이 자연스러운 흐름이라면 MVP 에서는 VM 이 View 에게 명령하여 방향이 반대로 흐르게 되고, 
MVC 에서는 Service 가 View, Repository 에게 모두 명령해서 또한 데이터 흐름이 일관되지 못하게 된다. 
따라서 두 (MVP, MVC) 는 데이터 Flow 가 부자연스럽다고 할 수 있다. 

 

 

 

 

예시 코드 

 

 

Entity

import Foundation

struct UtcTimeModel: Codable {
    let id: String
    let currentDateTime: String
    let utcOffset: String
    let isDayLightSavingsTime: Bool
    let dayOfTheWeek: String
    let timeZoneName: String
    let currentFileTime: Int
    let ordinalDate: String
    let serviceResponse: String?

    enum CodingKeys: String, CodingKey {
        case id = "$id"
        case currentDateTime
        case utcOffset
        case isDayLightSavingsTime
        case dayOfTheWeek
        case timeZoneName
        case currentFileTime
        case ordinalDate
        case serviceResponse
    }
}

 

 

Repository

import Foundation

class Repository {
    func fetchNow(onCompleted: @escaping (UtcTimeModel) -> Void) {
        let url = "http://worldclockapi.com/api/json/utc/now"

        URLSession.shared.dataTask(with: URL(string: url)!) { data, _, _ in
            guard let data = data else { return }
            guard let model = try? JSONDecoder().decode(UtcTimeModel.self, from: data) else { return }
            onCompleted(model)
        }.resume()
    }
}

 

 

Service

import Foundation

class Service {
    let repository = Repository()

    var currentModel = Model(currentDateTime: Date()) // state

    func fetchNow(onCompleted: @escaping (Model) -> Void) {
        
        // Entity -> Model
        repository.fetchNow { [weak self] entity in
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm'Z'"

            guard let now = formatter.date(from: entity.currentDateTime) else { return }

            let model = Model(currentDateTime: now)
            self?.currentModel = model

            onCompleted(model)
        }
    }

    func moveDay(day: Int) {
        guard let movedDay = Calendar.current.date(byAdding: .day,
                                                   value: day,
                                                   to: currentModel.currentDateTime) else {
            return
        }
        currentModel.currentDateTime = movedDay
    }
}

 

Model

import Foundation

struct Model {
    var currentDateTime: Date
}

 

 

 

 

ViewModel

import Foundation
import RxRelay

class ViewModel {
    
    let dateTimeString = BehaviorRelay(value: "Loading..")

    let service = Service()

    private func dateToString(date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy년 MM월 dd일 HH시 mm분"
        return formatter.string(from: date)
    }
    
    func reload() {
        // Model -> ViewModel
        service.fetchNow { [weak self] model in
            guard let self = self else { return }
            let dateString = self.dateToString(date: model.currentDateTime)
            self.dateTimeString.accept(dateString)
        }
    }

    func moveDay(day: Int) {
        service.moveDay(day: day)
        dateTimeString.accept(dateToString(date: service.currentModel.currentDateTime))
    }
}

 

 

 

View ( ViewController)

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet var datetimeLabel: UILabel!

    @IBAction func onYesterday() {
        viewModel.moveDay(day: -1)
    }

    @IBAction func onNow() {
        datetimeLabel.text = "Loading.."
        
        viewModel.reload()
    }

    @IBAction func onTomorrow() {
        viewModel.moveDay(day: 1)
    }

    let viewModel = ViewModel()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.dateTimeString
            .bind(to: datetimeLabel.rx.text)
            .disposed(by: disposeBag)

        viewModel.reload()
    }
}

 

 

출처: https://www.youtube.com/watch?v=M58LqynqQHc