[iOS] MVVM 패턴 정리
iOS

[iOS] MVVM 패턴 정리

728x90

MVC 패턴

  • Model에서 데이터 구조를 정의한다.
  • Controller에서 받아온 데이터를 가공한다.
  • View에서 가공한 데이터를 보여준다.

 

- 예제 코드

import UIKit

class ViewController: UIViewController {
    // MARK: - MODEL
    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
        }
    }

    // MARK: - CONTROLLER
    override func viewDidLoad() {
        super.viewDidLoad()
        fetchNow()
    }

    var currentDateTime = Date()

    private func updateDateTime() {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy년 MM월 dd일 HH시 mm분"
        datetimeLabel.text = formatter.string(from: currentDateTime)
    }

    private func fetchNow() {
        let url = "http://worldclockapi.com/api/json/utc/now"

        datetimeLabel.text = "Loading.."

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

            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm'Z'"

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

            self?.currentDateTime = now

            DispatchQueue.main.async {
                self?.updateDateTime()
            }
        }.resume()
    }

    // MARK: - VIEW
    @IBOutlet var datetimeLabel: UILabel!

    @IBAction func onYesterday() {
        guard let yesterday = Calendar.current.date(byAdding: .day,
                                                    value: -1,
                                                    to: currentDateTime) else {
            return
        }
        currentDateTime = yesterday
        updateDateTime()
    }

    @IBAction func onNow() {
        fetchNow()
    }

    @IBAction func onTomorrow() {
        guard let tomorrow = Calendar.current.date(byAdding: .day,
                                                   value: +1,
                                                   to: currentDateTime) else {
            return
        }
        currentDateTime = tomorrow
        updateDateTime()
    }
}

 

MVVM에서 Model 변환 과정

JSON에서

1️⃣ 서버 Model = Entity
-> UtcTimeModel
2️⃣ Model
-> Date(Model)
3️⃣ 화면 Model = ViewModel
-> String(화면)

순서로 변환됩니다.

정리하자면,

Repository

⬇️

Entitiy(Model)

⬇️

Mapper

⬇️

Model

⬇️

Service

⬇️

ViewModel(Model)

⬇️

View

이런식으로 나타낼 수 있습니다.

 

MVC -> MVVM

위의 MVC 코드를 MVVM로 변경해보겠습니다.

: Entitiy(Model), Repository, Model, Service, ViewModel(Model), View로 분리해줍니다.

Entity 서버로부터 온 모델
Repository Entity를 서버로부터 가져옴, 즉 서버 모델을 전달해줌
Model Service에서 취급하는 데이터
Service Repository를 사용해서 Entity(서버 모델)를 Model로 변환해줌 (Entity-> Model)
ViewModel Service를 사용해서 화면에 보여줘야할 값의 형태(ViewModel)로 변환해줌 (Model -> ViewModel)
View ViewModel이 변경되면 화면에 세팅해주는 작업 처리
화면 이벤트에 따라 ViewModel에 값 변경 요청

 

1. 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
    }
}

2. 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()
    }
}

3. Model

import Foundation

struct Model{
    var currentDateTime: Date
}

4. 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 moveDay = Calendar.current.date(byAdding: .day,
                                                            value: day,
                                                            to: currentModel.currentDateTime) else {
            return
        }
        currentModel.currentDateTime = moveDay
    }
}

5. ViewModel

import Foundation

class ViewModel {
    var onUpdated: () -> Void = {}
    
    var dateTimeString: String = "Loading.." // 화면에 보여져야할 값 // View를 위한 Model: ViewModel
    {
        didSet {
            onUpdated()
        }
    }
    
    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 = dateString

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

6. View

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var datetimeLabel: UILabel!
    
    @IBAction func onYesterday(_ sender: Any) {
        viewModel.moveDay(day: -1)
    }
    @IBAction func onNow(_ sender: Any) {
        datetimeLabel.text = "Loading.."
        viewModel.reload()
    }
    @IBAction func onTomorrow(_ sender: Any) {
        viewModel.moveDay(day: 1)
    }
    
    let viewModel = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.onUpdated = {[weak self] in
            DispatchQueue.main.async {
                self?.datetimeLabel?.text = self?.viewModel.dateTimeString
            }
        }
        
        viewModel.reload()
    }

}

 

MVVM 패턴

  • view는 viewModel에 의존해서 화면을 그려낸다.
  • viewModel은 서비스에 의존해서 Model을 가져온다.
  • Model은 Repository로부터 얻어온 Entity를 가지고 만들어낸다.

 

[출처] 

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

728x90