제목: Protocols and MVVM in Swift to avoid repetition
우리가 Viable을 최신 iOS 앱의 토대를 만들어갈때, 이전 iOS 앱으로부터 배우려 했다. 우리는 2가지 목표를 정했다.
- Massive View Controller(MVC를 비꼬는 약자) 증후군 피하기
- 가능한 적은 중복
초기에 디자인팀이 만든 Viable 화면에는 수많은 비슷한 화면이었다. 아래에 간단하게만든 예시를 한번 보자. 두 화면은 모두 상단에 UILabel이 있고 검색 결과를 보여주는 UITableView가 있다. 각각의 결과에대한 UITableViewCell도 매우 비슷했다. 이들은 다소 레이아웃을 공유했고 데이터만 달랐다.
Viable은 화면에 표시되는 6가지 타입의 데이터가 있었으며, 각 타입마다 새로운 뷰 컨트롤러를 만들어서 코드 중복이 많았다. 그리하여 우리는 6개의 데이터 타입을 모두 표시할 수 있는 SearchResultsViewcController를 만들었다.
데이터 타입에따라 다르게 렌더링하기위해 제일 처음 떠오른 방법으로는, tableView:cellForRowAtIndexPath:에 거대한 if/else문이었는데, 코드 규모가 잘 정연되지 못했고 결국 길고 못난 메소드가 되버렸다.
MVVM와 프로토콜을 사용하여 해결하기
테일러 구이던(Taylor Guidon)은 MVVM(Model-View-ViewModel) 패턴에대한 입문의 글을 포스팅했는데, 여기서 확인할 수 있다. 이 글은 그 요약 버전인데, 데모 프로젝트에 적용한 것을 깃헙에서 확인할 수 있다.
모델(Models)
모델 그룹에서의 모델은 데이터를 담고 있는다. 우리는 DomainModel과 ProductModel을 가지는데, 둘 다 구조체이다. DomainModel은 이름(name)과 그 상태 도메인을 가질것이고, ProductModel은 제품이름(product name), 제품평점(product rating), 제품로고(product logo), 제품가격(product price)을 가진다.
struct Product {
var name: String
var rating: Double
var price: Double?
}
뷰모델(View Models)
모든 데이터 모델은 해당되는 뷰모델을 가진다. 그 말은, 우리 예제에서는 DomainViewModel과 ProductViewModel을 가진다는 뜻이다. 뷰모델은 모델로부터 데이터를 받아서 사용자에게 보여주기전에 뷰에 적용시킨다. 예를들어 ProductViewModel은 4.99 가격의 부동소수점을 받아서 $4.99라 읽히는 문자열로 변형한다.
class ProductViewModel: CellRepresentable {
var product: Product
var rowHeight: CGFloat = 80
var price: String {
guard let price = product.price else {
return "free"
}
return "$\(price)"
}
init(product: Product) {
self.product = product
}
func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
// Dequeue a cell
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductTableViewCell
// Pass ourselves (the view model) to setup the cell
cell.setup(vm: self)
// Return the cell
return cell
}
}
뷰(Views)
우리 예제에서 뷰는 두가지 UITableViewCell이다. DomainTableViewCell과 ProductTableViewCell를 가진다. 레이아웃은 앱의 스토리보드에 만들어놓았따. 두 클래스 모두 간단한데, 뷰모델을 인자로 받는 setup 메소드 하나만 가지고 있다. 뷰모델은 셀에 정보를 옮길때 사용되는데, 예를들자면 읽을 수 있는 가격($4.99)을 받아서 UILabel의 테스트 프로퍼티에 할당한다.
class ProductTableViewCell: UITableViewCell {
func setup(vm: ProductViewModel) {
self.textLabel?.text = vm.product.name
self.detailTextLabel?.text = vm.price
}
}
합쳐보기
3가지 큰 기둥을 만들었으니 합쳐보자. 뷰 컨트롤러와 뷰모델을 합치기위해 프로토콜을 사용할 것이다. 프로토콜은 이것을 따르는 클래스나 구조체가 어떤 변수와 메소드를 가질지 정의한다. 계약서를 생각해보자. 여러분이 X라는 프로토콜을 따르고 싶다면, 여기에 명시된 모든것을 구현해야한다. 간결하게 만들기위해 한 프로퍼티와 한 메소드만 넣어놨다. DomainViewModel과 ProductViewModel 둘 다 이 프로토콜을 따른다.
protocol CellRepresentable {
var rowHeight: CGFloat { get }
func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell
}
스위프트에서 프로토콜은 일급 객체(first class citizen)이므로 SearchResultsViewController 파일은 화면에 표시할때 필요한 뷰모델 배열을 가진다. [DomainViewModel]()이나 [ProductViewModel]()처럼 배열을 초기화하는것 대신, 프로토콜을 사용하여 뷰모델을 담아둘 수 있다. var data = [CellRepresentable](). DomainViewModel과 ProductViewModel은 CellRepresentable을 따르기 때문에 배열은 둘 다 담아둘 수 있다.
이제 배열에 있는 모든 요소를 CellRepresentable을 따르게하여 UITableViewCell을 반환하는 cellInstance(_ tableView: UITableView, indexPath: IndexPath) 메소드를 가진다고 확신하게 만들자. 고맙게도 tableView:cellForRowAtIndexPath:는 cellInstance 메소드만 호출하면 된다.
extension SearchresultsViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return data[indexPath.row].cellInstance(tableView, indexPath: indexPath)
}
}
extension SearchresultsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return data[indexPath.row].rowHeight
}
}
이게 전부다. 우리는 다양한 셀의 다양한 열 높이로 표시해주는 작은 뷰컨트롤러를 가지게 되었다! ISL의 깃헙 페이지에서 데모 프로젝트를 확인해볼 수 있다. 제안이나 질문이 있다면 주저하지말고 @thomasdegry에 트윗해달라.
이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은
- 블로그 댓글
- 페이스북 페이지(@나는한다번역)
- 이메일(canapio.developer@gmail.com)
- 트위터(@canapio)
으로 보내주시면 됩니다.
'Swift와 iOS > 아키텍처' 카테고리의 다른 글
[번역]스위프트에서 Task 고립시키기, 혹은 어떻게 테스트가능한 네트워크 레이어를 만들지 (0) | 2017.08.14 |
---|---|
[번역]MVP와 MVC가 무엇이며, 그 차이는 무엇입니까? - StackOverflow (3) | 2017.05.11 |
[번역]VIPER 아키텍처로 iOS 앱 만들기 (1) | 2017.05.09 |
[번역]코코아에서 본 Model-View-Controller (0) | 2017.05.09 |
[번역] 어떻게 네트워크 레이어를 만들 수 있을까? (0) | 2016.12.29 |
WRITTEN BY
- tucan.dev
개인 iOS 개발, tucan9389
,