본문 바로가기

ComputerScience/ios App(Storyboard)

ios - 15 Design Pattern

728x90

1 Design Pattern

- 후에 유지보수를 쉽게하기 위해 일반화한 개발 패턴을 말한다.

- 코드의 중복을 줄이고 각 객체들이 단일 책임을 갖도록 하기 위한 방법론 모두가 디자인 패턴에 속한다.

- 일반적으로 올바른 프로그램의 구조 혹은 올바른 개발을 위한 가이드, 전략으로 활용된다.

- 재사용과 지속가능한 개발을 가능하게 하고 기술부채를 줄이는데 목적이 있다.

- 이번 장에서는 모바일 개발에 있어서 많이 사용되는 디자인 패턴들을 공부할 것 이다.

2 MVC

- Model - View - Controller

- 이전에 앱 개발에 있어서 널리 활용되던 디자인 패턴이다.

- 데이터와 UI를 controller가 중계하는 모델이다.

- 데이터의 변화 혹은 UI의 변화를 항상 controller가 인지하고 변화를 주도한다.

- controller에 부하가 쌓이기 쉽다. 즉 너무 많은 일을 감당하려고 하는 것이다.

3 MVVM

- Model - View - ViewModel

- 더 발전된, 널리 채택되어 사용되는 방식이다.

- 기존의 View Controller의 많은 역할을  View Model이라는 class로 위임하면서 view controller의 역할을 축소한다.

- class로 분리할 수 있다는 것은 그 역할을 분명히 표현할 수 있다는 뜻이고 이는 유지보수와 직결된다.

4 MVVM 적용하기

- 이전에 만들었던 앱에 새로운 디자인 패턴을 적용하여 refactoring 해보자

*BountyViewController 분해

Model View ViewModel
Bounty Info
- 현상금, 이름의 정보를 struct로 묶어서 객체로 관리하자
- View Model이 이 형식의 데이터들을 소유하고 있다.
ListCell
- 앞으로 ListCell은 정보를 ViewModel로 부터 받아와서 화면에 보여줄 것이다.
- View Controller가 View와 Model에 직접 접근하면 안된다.
- 따라서 그 역할을 대신 수행할 BountyViewModel이라는 클래스를 만들고 vc가 이 객체를 소유할 것이다.
import UIKit

struct BountyInfo {
    let name: String
    let bounty: Int
    
    var image: UIImage? {
        return UIImage(named: "\(name).jpg")
    }
    
    init(name: String, bounty: Int) {
        self.name = name
        self.bounty = bounty
    }
}

- 현상금, 이름을 struct로 묶어서 객체로 관리하자.

class BountyViewModel {
    let bountyInfoList: [BountyInfo] = [
        BountyInfo(name: "brook", bounty: 33000000),
        BountyInfo(name: "chopper", bounty: 50),
        BountyInfo(name: "franky", bounty: 44000000),
        BountyInfo(name: "luffy", bounty: 300000000),
        BountyInfo(name: "nami", bounty: 16000000),
        BountyInfo(name: "robin", bounty: 80000000),
        BountyInfo(name: "sanji", bounty: 77000000),
        BountyInfo(name: "zoro", bounty: 120000000)
    ]
    
    var sortedList: [BountyInfo] {
        let sortedList = bountyInfoList.sorted { prev, next  in
            return prev.bounty > next.bounty
        }
        
        return sortedList
    }
    
    var numOfBountyInfoList: Int {
        return bountyInfoList.count
    }
    
    func bountyInfo(at index: Int) -> BountyInfo {
        return sortedList[index]
    }
}

- BountyViewModel은 bounty view controller가 사용할 데이터들을 가지고 있고 뷰컨트롤러와 view들은 BountyViewModel로 부터 데이터를 받아올 것이다. 이에 필요한 메서드들이 정의되어 있다.

import UIKit

class BountyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    let viewModel = BountyViewModel()
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.numOfBountyInfoList
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? ListCell else {
            return UITableViewCell()
        }
        let bountyInfo = viewModel.bountyInfo(at: indexPath.row)
        cell.update(info: bountyInfo)
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("---->  \(indexPath.row)")
        performSegue(withIdentifier: "showDetail", sender: indexPath.row)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showDetail" {
            let vc = segue.destination as? DetailViewController
            if let index = sender as? Int {
                let bountyInfo = viewModel.bountyInfo(at: index)
                vc?.viewModel.update(model: bountyInfo)
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

class ListCell: UITableViewCell {
    @IBOutlet weak var imgView: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var bountyLabel: UILabel!
    
    func update(info: BountyInfo) {
        imgView.image = info.image
        nameLabel.text = info.name
        bountyLabel.text = "\(info.bounty)"
    }
}

- bounty view controller는 bountyViewModel객체와 view(ListCell)를 소유하고 있다.

- view와 viewModel만이 소통이 이루어진다.

- ListCell을 독립된 파일로 분리해도 좋으나 이렇게 코드가 짧은 경우는 연관있는 파일에 모아 놓기도 한다.

*DetailViewController 분해

Model View ViewModel
Bounty Info
- 이전 스크린에서 클릭한 데이터를 viewModel이 소유하고 있는 model을 통해 넘겨받을 것이다.
imgView, nameLabel, bountyLabel - DetailViewModel이 DetailVeiwController의 역할 일부를 대신할 것이다.
- model을 가지고 view와 소통하기 위한 메서드를 가지고 있다.
import UIKit

class DetailViewModel {
    var bountyInfo: BountyInfo?
    
    func update(model: BountyInfo?) {
        bountyInfo = model
    }
}
import UIKit

class DetailViewController: UIViewController {
    @IBOutlet weak var imgView: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var bountyLabel: UILabel!
    
    let viewModel = DetailViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI()
    }
    
    func updateUI() {
        if let bountyInfo = viewModel.bountyInfo {
            imgView.image = bountyInfo.image
            nameLabel.text = bountyInfo.name
            bountyLabel.text = "\(bountyInfo.bounty)"
        }
    }
    
    @IBAction func close(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
}

- 이전 스크린에서 클릭한 bountyInfo를 veiwModel을 통해 넘겨 받았다.

- DetailViewController가 DetailViewModel을 가지고 있다.

 

- 이전에는 view controller가 모든 역할을 수행했지만 새로운 디자인 패턴을 적용하고 나서는 역할이 쪼개진 것을 알 수 있다.

- 데이터(model)를 가지고 view와 소통하는 역할은 viewModel이 수행한다.

- view controller라는 곳에서 view와 viewModel이 서로의 소통한다.

728x90
반응형

'ComputerScience > ios App(Storyboard)' 카테고리의 다른 글

ios - 17 Animation with contraints  (0) 2021.08.03
ios - 16 CollectionView  (0) 2021.08.03
ios - 14 Segue  (0) 2021.07.28
ios - 13 Custom Cell  (0) 2021.07.28
ios - 12 Table View, Table View Cell  (0) 2021.07.25