본문 바로가기
iOS/Architecture

iOS - MVVM Simple look

by HaningYa 2020. 4. 22.
728x90

[원문]

 

MVVM in iOS

Reduce the size of your massive view controller by moving logic into the view model

medium.com

로직부분을 viewmodel 로 옮겨 MVC(Massive view controller) 의 크기를 줄이자.

  • 기본 iOS 아키텍쳐 패턴은 MVC(Model View Controller) 이다.
  • MVC패턴이 잘못된 것은 아니지만 개발하다 보면 view controller의 코드량이 많아진다.
  • MVVM은 .NET 커뮤니티에서 주로 몇년째 사용되었다.
  • .NET WPF 프레임워크와 iOS 프레임워크는 다르다.(WPF는 XAML을 통해 two-way seamless binding 을 지원한다)
  • iOS는 그런거 없고 declarative UI code도 없다(SwiftUI는?)
  • MVVM 패턴의 핵심 아이디어는 화면에 표시되는 각각의 View가 해당 View의 data를 제공해 주는 View Model 에 의해 backed 된다는 것이다.
  • 이번에 작업해 볼 간단한 앱은
    - UITableView : news를 표시하는 테이블뷰
    - view model : title, description, publishedDate, author, source 등의 news data
    로 이루어 질 것이다.

뷰를 먼저 짠다.

UITableView와 추가 버튼으로 간단하게 구성한다.

 

이제 뷰에 뿌려질 데이터에 대해 생각해본다.

단순함을 위해 title과 description을 표시할 것이다. 그래서 view model은 title 과 description을 properties로 가질것이다.

 

struct ArticleViewModel {
    
    var title :String
    var description :String
}

extension ArticleViewModel {
    
    init(article :Article) {
        self.title = article.title
        self.description = article.description
    }
}

이런식으로 view model을 구성하면 view 전체에 대한 데이터를 표현하지 못한다.(title과 descriptio은 cell의 데이터를 표현)

 

UITableView에 대한 데이터를 표시하려면

struct ArticleListViewModel {
    
    var title :String? = "Articles"
    var articles :[ArticleViewModel] = [ArticleViewModel]()
}

extension ArticleListViewModel {
    
    init(articles :[ArticleViewModel]) {
        self.articles = articles
    }
    
}

이런식으로 구성해야한다. 

 

View Models + Networking

MVVM을 iOS에 적용할때 많은 개발자들이 네트워킹과 데이터 접근 코드를 view model 에 구현하는 걸 볼 수 있었다.

예를 들면

struct ArticleListViewModel {
    
    var title :String? = "Articles"
    var articles :[ArticleViewModel] = [ArticleViewModel]()
}

extension ArticleListViewModel {
    
    init(articles :[ArticleViewModel]) {
        self.articles = articles
    }
    
    func loadArticles(callback :(([Article]) -> ())) {
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            
            if let data = data {
                
                let json = try! JSONSerialization.jsonObject(with: data, options: [])
                let dictionary = json as! JSONDictionary
                
                let articleDictionaries = dictionary["articles"] as! [JSONDictionary]
                
                articles = articleDictionaries.flatMap { dictionary in
                    return Article(dictionary :dictionary)
                }
            }
            
            DispatchQueue.main.async {
                callback(articles)
            }
    }
}

 

네트워크 함수인 loadArticle을 ArticleListViewModel에 구현한 것이다.

이런식으로 구현하면 view model 레이어와 web service 레이어의 coupling이 tight하게 될 수 있다.

 

하지만 view model은 최대한 기능이 단순해야하기 때문에 networking이나 data access 관련 코드가 없어야 한다.

 

예를 들어 아래 코드는 view controller 안에 있지만 web service 레이어로 나뉘어진 코드이다.

private func loadArticles() {
        
        // this url should be part of the URL builder scheme and not right inside the
        // view controller but right now we are focused on MVVM
        let url = URL(string: "https://newsapi.org/v1/articles?source=the-next-web&sortBy=latest&apiKey=0cf790498275413a9247f8b94b3843fd")!
        
        // this web service should use generic types. Again this is not part of the implementation
        // as we are focusing on MVVM model
        Webservice().getArticles(url: url) { articles in
            print(articles)
            
            let articles = articles.map { article in
                return ArticleViewModel(article :article)
            }
            
            self.viewModel = ArticleListViewModel(articles :articles)
            
        }
    }
    

위 코드에서 web service는 view model 객체에 mapped 된 domain object를 리턴한다. 이 mapping은 직접 해도되고 AutoMapper같은 자동 매핑 툴을 사용해도 된다.

 

마지막으로 mapping이 끝나면 view model 이 UITableView를 reload 시킬 트리거를 만든다. 이 트리거는 자동으로 되는데 didSet 코드 때문이다. 

*didSet은 class 의 constructor 안에서는 트리거되지 않는다.

MVVM Bindings

바인딩은 View와 View Model의 data 의 flow를 말한다.

 

예를들어 등록 화면을 만들때 정보를 text field에 넣을때 바로 RegistrationViewModel 객체가 만들어지는 것이다.

그럼 아래의 코드를 쓸 필요가 없어진다.

self.viewModel.title = self.titleTextField.text!
        self.viewModel.description = self.descriptionTextField.text!

 

Binding은 양방향이다. 이 뜻은 view model 을 바꿔도 view에 반영이 된다는 뜻이다. 

 

string type을 observe 할 수 없어서 custom class 를 만든다.

import Foundation

class Dynamic<T> {
    
    var bind :(T) -> () = { _ in }
    
    var value :T? {
        didSet {
            bind(value!)
        }
    }
    
    init(_ v :T) {
        value = v
    }
    
}

Dynamic<T>는 T타입의 value를 가질 수 있다. value가 변화되면 didSet이 실행되고 bind function 을 호출하여 value를 caller로 주게 된다.

 

class AddArticleViewController : UIViewController {
    
    // For this example assume they are UITextField
    @IBOutlet weak var titleTextField :BindingTextField!
    @IBOutlet weak var descriptionTextField :BindingTextField!
    
    var viewModel :AddArticleViewModel! {
       
        didSet {
            viewModel.title.bind = { [unowned self] in self.titleTextField.text = $0 }
            viewModel.description.bind = { [unowned self] in self.descriptionTextField.text = $0 }
        }
    }
    
    @IBAction func AddArticleButtonPressed(_ sender: Any) {
        
        self.viewModel.title.value = "hello world"
        self.viewModel.description.value = "description"
    }
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        self.viewModel = AddArticleViewModel()
    }
    
}

이 코드는 한방향의 바인딩 (view model 로 부터 ui로) 을 세팅해준다. 이말은 view model 이 바뀌면 ui도 notified 된다는 것이다.

양방향으로 하고 싶다면 custom UITextField control을 구현해야 한다.

import Foundation
import UIKit

class BindingTextField : UITextField {
    
    var textChanged :(String) -> () = { _ in }
    
    func bind(callback :@escaping (String) -> ()) {
        
        self.textChanged = callback
        self.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
    }
    
    @objc func textFieldDidChange(_ textField :UITextField) {
        
        self.textChanged(textField.text!)
    }
    
}

위와같이 editingChanged 이벤트를 UITextField에 붙이고 custom callback 함수를 만든다. 그럼 IBOutlets를 아래와 같은 방법으로 update할 수 있다.

 

 @IBOutlet weak var titleTextField :BindingTextField! {
        didSet {
            titleTextField.bind { self.viewModel.title.value = $0 }
        }
    }
    @IBOutlet weak var descriptionTextField :BindingTextField! {
        didSet {
            descriptionTextField.bind { self.viewModel.description.value = $0 }
        }
    }
    

결론

MVVM 모델의 단순한 버전

MVVM은 logic test를 편하게 해줌

unit tests하기 쉽게 해줌


다읽어 봤으니 한번 만들어 보자.

원래 프로젝트(아래 링크)는 다른 기능까지 있는 코드이지만 나는 단순히 뉴스 헤드라인만 받아와서 리스트에 뿌리는 기능만 구현했다.

 

azamsharp/HeadlinesMVVM

Contribute to azamsharp/HeadlinesMVVM development by creating an account on GitHub.

github.com

[리스트 populate  만 있는 프로젝트]

 

KimTaeHyeong17/iOS-MVVM-SimpleProject

간단한 기능의 앱으로 MVVM 패턴 연습. Contribute to KimTaeHyeong17/iOS-MVVM-SimpleProject development by creating an account on GitHub.

github.com

1. 새 Xcode 프로젝트를 생성한다.

이름은 엿장수 마음대로


2. 기본적인 View를 짠다.

*UITableVIewControler로 해야한다.(예제가 그렇게되있다)


cell identifier 을 'Cell' 로한다. (ArticleListViewTableViewController에서 identifier를 Cell로 했기때문)

3. 프로젝트 디렉토리 정리 한다.

밑에 swift파일 앞으로 추가할것이다.

 


 

4. Controllers - ArticleListTableViewController.swift

//
//  AddArticleViewController.swift
//  SimpleNewsApp_MVVM
//
//  Created by 김태형 on 2020/04/23.
//  Copyright © 2020 김태형. All rights reserved.
//

import Foundation
import UIKit

class ArticleListTableViewController : UITableViewController {
    
    private var viewModel :ArticleListViewModel = ArticleListViewModel()  {
        
        didSet {
            self.tableView.reloadData()
        }
    }
    
    
    var didSelect: (ArticleViewModel) -> () = { _ in }
    var addArticleTapped: () -> () = {}
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.title = self.viewModel.title
        
        loadArticles()
    }
    
    private func loadArticles() {
        
        // this url should be part of the URL builder scheme and not right inside the
        // view controller but right now we are focused on MVVM
        let url = URL(string: "https://newsapi.org/v1/articles?source=the-next-web&sortBy=latest&apiKey=0cf790498275413a9247f8b94b3843fd")!
        
        // this web service should use generic types. Again this is not part of the implementation
        // as we are focusing on MVVM model
        Webservice().getArticles(url: url) { articles in
            print(articles)
            
            let articles = articles.map { article in
                return ArticleViewModel(article :article)
            }
            
            self.viewModel = ArticleListViewModel(articles :articles)
            
        }
    }
 
    @IBAction func addArticleButtonTapped(_ sender: Any) {
        
        addArticleTapped()
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        let articleViewModel = self.viewModel.articles[indexPath.row]
        didSelect(articleViewModel)
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.viewModel.articles.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let articleViewModel = self.viewModel.articles[indexPath.row]
        
        cell.textLabel?.text = articleViewModel.title
        return cell
    }
}

5. Models - Article.swift

6. View Models - ArticleListViewModel.swift

7. Web Servives - Webservice.swift

8. storyboard 랑 ArticleListTableVIewController 연결해준다.

10. 실행시키면 뉴스를 들고오는 것을 볼 수 있다.


내가 이해한 데로 요약하자면 

프로젝트에는

  • Controllers : view controllers, WebService의 getArticle을 호출해 viewModel을 만들어서 UI 에 뿌려주는 역할(데이터는 모름)
  • WebServices : api에서 json을 받아와 Article.swift 에 작성된 모델형태로 객체반환(view controller와 네트워크 레이어 분리를 위해)
  • ViewModels : Models 데로 파싱한 걸 view controllers에서 인스턴스화 해서 UI에 뿌려질 데이터를 가지고 있는 역할
  • Models : 파싱할 데이터 json 구조
  • Storyboard : storyboard, UI

결론적으로 view model 이 없으면 예를 들어 

ArrayList<Article> articleArraylist = new ArrayList() 

로 Article 데이터를 배열로 가져서 데이터가 업데이트 될 때마다

//if articleArrayList changed
tableview.reloadData()

해줘야 되는건데 이걸 view model을 만들어서 view model 이 업데이트 될 때 마다 didSet을 통해 자동으로 tableview 를 reload 해주고

private var viewModel :ArticleListViewModel = ArticleListViewModel()  {
        
        didSet {
            self.tableView.reloadData()
        }
    }

view controller 에 서 UI로 뿌려질 데이터를 모두 view model 로 처리하는

//처음 데이터 받아올떄 부터 viewModel에 넣는다.
Webservice().getArticles(url: url) { articles in
            print(articles)
            
            let articles = articles.map { article in
                return ArticleViewModel(article :article)
            }
            
            self.viewModel = ArticleListViewModel(articles :articles)
            
        }

느낌이든다.

 

사실 잘 모르겠다.

 

 

iOS 아키텍처 패턴(MVC, MVVM, VIPER)

Overview“글 한 번 써보실래요?” 입사하고 일주일이 지나 기술 블로그에 글을 써 보라는 제안을 받았습니다. 여러 고민 끝에, 아이폰 앱(이하 ‘iOS’) 주니어 개발자로서 프로젝트 경험과, 공부한 내용을 바탕으로 글을 쓰기로 했습니다.

labs.brandi.co.kr

 

 

iOS Architecture Patterns

Demystifying MVC, MVP, MVVM and VIPER

medium.com

 

728x90

댓글