[UIKit][UITableView] MVC패턴으로 UITableView 구현

Uno·2021년 12월 4일
2

UIKit

목록 보기
7/9

MVC 패턴


  • 모델 (Model)
    - 모델은 데이터은 데이터가 있는 곳 입니다. 예를들면, 서버에서 받아온 데이터가 있습니다.

  • 뷰 (View)
    - 우리가 앱을 보면, 눈으로 보는 부분을 담당하는 레이어를 “뷰 “라고 부릅니다. 예를들면, UILabel, UIView와 같은 것들이 있겠죠.

  • 컨트롤러 (Controller)
    - 컨트롤러는 뷰와 모델을 제어(Control) 합니다. 델리게이션 패턴을 사용하기도 하고 다른 방식으로 컨트롤하기도 합니다. UITableView를 기준으로 예시를 들면, 컨트롤러는 구체적인 UITableView가 어떻게 생겼는지 전혀 모릅니다. 그저 UITableViewDelegateUITableViewDataSoure 라는 프로토콜을 통해서 값만 전달해줄 뿐이죠.

그림으로보면 다음과 같습니다.

1. 사용자의 상호작용

  • 사용자가 UIButton을 클릭하거나 하는 동작을 말합니다.
  • 그러면, 버튼을 클릭했다는 신호를 뷰 -> 컨트롤러 로 전달합니다.
  1. 모델의 데이터 업데이트
  • 모델에 있는 데이터를 변경합니다.
  • 예를들면, UISwitch를 의 isOn 을 true -> false 로 변경할 수 있겠죠.
  1. 업데이트 된 데이터 전달
  • 2번 과정으로 인해 데이터가 변경되었으니 최신 데이터를 컨트롤러에 전달합니다.
  1. UI 업데이트
  • 모델의 데이터가 변경되자마자 뷰를 업데이트해줍니다.

  • 마치, 프로필 수정했으면, 수정된 데이터가 앱에 보여야 겠죠. 그런 로직을 의미합니다.

    MVC 패턴을 자신의 프로젝트에 적용한다고 해서, 절대로 깨면안되는 규칙처럼 엄격하게 적용할 필요는 없습니다. 그건 MVC 패턴 그 자체에 너무 집착하는 것이죠. MVC 패턴을 통해 달성하려는 목적에 집중해야합니다.

MVC 패턴을 통해 얻고 싶은 것

  • 역할 및 코드 의 분리
  • 그로 인한 가독성 향상 (협업 용이)
  • 모듈성 향상

이미 공부를 하시거나, 들은 것들이 있으신 분들은 MVC 패턴이 완벽한 패턴이 아닌 것을 아실겁니다. 이유는 애매한 영역에 속하는 코드들이 나타나기 때문에, 그로 인해 MVC의 구분이 희미해지거나 한쪽이 극단적으로 비대해지는 현상이 있기 때문이죠. 하지만 MVC 패턴에 대한 공부는 다른 패턴에 대한 공부의 시작점으로는 좋은 역할을 하니 한 번 익혀봅시다.^^

Model


저는 이번 MVC를 테이블뷰를 통해서 설명하고자 합니다. 그래서 모델의 이름을 “TableViewModel” 이라고 명명했습니다.

class TableViewModel {
    ...
}

테이블 뷰에 필요한 데이터가 어떤 것이냐에 따라서 “TableViewModel” 이 구성되겠네요.

전 “전화번호부 앱” 을 만들고 싶습니다. 그러므로 이름과 전화번호를 받을 데이터 구조체를 선언하겠습니다.

struct PeopleInfo {
    var name: String
    var cellPhoneNumber: Int
    
    init(name: String, cellPhoneNumber: Int) {
        self.name = name
        self.cellPhoneNumber = cellPhoneNumber
    }
}
  • PeopleInfo 라는 이름으로 구조체를 선언했습니다. 앞으로 이 타입을 통해서 데이터를 주고받을 생각입니다.

이렇게 구조체를 선언했으면, 데이터를 관리하는 모델에 이것들을 선언하겠습니다.

우리가 주소록을 만들 때, 여러명의 주소를 기록해두죠. 그러므로 PeopleInfo 를 여러개 보관해야합니다.

이처럼 동일한 타입이지만, 여러개의 데이터를 관리하는 자료구조가 Array 이죠. 그러면 Array를 사용하여 정의하겠습니다.

class TableViewModel {
    
    var storage: [PeopleInfo]
    
    init() {
        self.storage = [PeopleInfo]()
    }
}

storage 라는 이름이고 타입은 [PeopleInfo] 입니다. 이제 이곳에 데이터를 저장할겁니다.

다만, 제가 예상하지 못한 곳에서 (스스로의 실수일 확률 혹은 동료의 미스커뮤니케이션) 원본데이터가 손상되면 안되겠죠.

그래서 접근제한을 통해 storage를 선언하겠습니다.

private var storage: [PeopleInfo]

이제 storage 라는 데이터는 TableViewModel 내부에서만 수정이 가능합니다. 데이터가 변경 될 가능성을 축소시키면서 데이터의 신뢰성을 보장할 있죠.

여기에 별명(typealias)를 이용해서 이 데이터는 셀에 넣기 위한 데이터임을 좀더 명확하게 표시하려고 합니다. 다음 코드처럼요.

class TableViewModel {
    typealias CellData = PeopleInfo
    
    private var storage: [CellData]
    
    init() {
        self.storage = [CellData]()
    }
}
  • 코드의 로직이나 무언가 변경된 것은 아닙니다. 다만 이름을 바꿔부르고 싶었을 뿐입니다.
  • 2달 뒤의 자기가 보더라도 “아! 이거 셀에 넣을라고 만들었지?!” 이런 생각을 할 수 있도록 말이죠.
    그냥 주석으로 쓰셔도 됩니다.

데이터를 담을 “틀” 만 만든 상태입니다. 전화번호부가 처음에만 데이터를 추가할 수 있고 나중에 추가 못한다는 것은 말이 안되겠죠? 그러므로 데이터를 추가하는 함수를 “TableViewModel” 내부에 만들어 줍니다.

    public func addItem(_ value: CellData) {
        self.storage.append(value)
    }
  • storage에 append를 해준 것으로 배열에 데이터를 추가합니다.
  • 이름이 addItem으로 한 이유는 추후에, 프로토콜을 통해서 정의하고자 하려고 합니다.

나중에 컨트롤러가 특정 데이터를 달라고 할 수도 있겠죠. 데이터 조회 를 위한 메소드를 구현하겠습니다.

    public func itemAt(index: Int) -> CellData? {
        return self.storage[index]
    }
  • 입력받은 index 값을 storage에 접근해서 해당 인덱스에 해당하는 값을 리턴하면됩니다.

여기서 문제가 있습니다. 만약 입력받은 index가 storage의 count 보다 크다면?
out of range 에러가 발생할 겁니다.

이것을 방지하기 위해서 연산프로퍼티를 통해서 count 값을 받고,
storage의 갯수(count)와 입력받은 인덱스(index) 를 비교하여 인덱스가 더 큰 경우 데이터를 반환하지 않도록 하겠습니다.

    var count: Int {
        storage.count
    }

    public func itemAt(index: Int) -> CellData? {
        guard count > index else { return nil }
        return self.storage[index]
    }

outOfRange로 인한 크래쉬 가능성을 하나 막았습니다.

삭제는 나머지를 구현한 이후에 돌아와서 기능구현하도록 하겠습니다.

여기까지가 TableViewModel 의 구현이였습니다.

View


이제 화면에 보여줄 UI를 배치하겠습니다.

순서
1. 테이블 뷰를 뷰컨트롤러에 추가한다.
2. 테이블 뷰에 들어갈 셀(cell) 의 UI를 배치한다.
3. 테이블 뷰와 뷰 컨트롤러를 IBOutlet으로 연결한다.
4. 테이블뷰 프로토콜을 통해서 테이블 뷰의 구체적인 구현을 대신 구현하도록 처리한다.
5. 테이블 뷰에 셀을 등록한다.

1. 테이블 뷰를 뷰컨트롤러에 추가한다.

cmd + shift + L 을 클릭해서 라이브러리를 호출합니다. 그리고 UITableView라고 검색합니다.

드래그 앤 드랍으로 ViewController 위에 둡니다.

그리고 오토레이아웃을 통해 위치를 조절합니다.(원하시는대로하시면되요.)


저는 모두 0으로 설정했습니다.

자 그러면 아래 그림처럼 꽉차있겠죠?

2. 테이블 뷰에 들어갈 셀(cell) 의 UI를 배치한다.

이제 UITableviewCell 을 생성해보겠습니다.
(좌측 파일 프로젝트 네비게이터 클릭 후) cmd + n 을 눌러서 새로운 파일을 생성합니다.
파일의 형식은 Cocoa Touch Class 입니다.

next를 누르시고, 보시면

  • class
  • subclass of
  • (체크박스) Also create XIB file
  • Language

이렇게 4 가지 항목이 있습니다.

subclass of 에 “UITableViewCell” 이라고 작성합니다. 그리고 테이블 뷰 셀이름을 원하시는 이름으로 설정하시면 됩니다.

그리고 Also crate XIB file 체크박스를 체크합니다. 아래 그림처럼요.

그리고 next를 누르고 Create 를 눌러줍니다.

그러면 프로젝트 네비게이터아 위 그림처럼 2 개의 파일이 생성되었습니다. 하나는 .Swift 다른 하나는 .xib 파일입니다. 마치 ViewController.swift 와 Main.storyboard 에 있는 ViewController 와 같습니다.

아주 간단하게 UITableViewCell의 UI를 배치하겠습니다.

3. 테이블 뷰와 뷰 컨트롤러를 IBOutlet으로 연결한다.

이제 테이블 뷰도 ViewController에 추가하겠습니다.

지금 IBOutlet 으로 연결하는 과정이 Controller와 View를 연결하는 방식입니다!

4. 테이블뷰 프로토콜을 통해서 테이블 뷰의 구체적인 구현을 대신 구현하도록 처리한다.

  • 테이블 뷰를 우리가 직접 만드는게 아니죠? 이미 애플에서 만들어준 코드를 우리는 가져올 뿐입니다.
  • 가져오는 방법은. 우리가 만든 아래 그림 테이블 뷰 있죠?

    이 테이블 뷰가 애플이 만든 로직대로 동작하도록 해주어야 합니다.
    그 과정은 아래와 같습니다.
  1. 현재 ViewController가 (애플이만든)프로토콜을 따른다고 알려준다.
  • UITableViewDelegate (protocol) & UITableViewDataSoure (protocol)
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
    }
  1. 그리고 ViewController에 해당 프로토콜에서 필수구현해야하는 메소드 2 개를 추가한다.
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}
  • 지금 코드를 보시면 2 개의 각각의 메소드에 return을 하드코딩을 통해 넣어둔게 보이실겁니다. 에러메세지 보기 싫어서 일단 넣었구요. 우리가 만든 데이터모델이 들어가도록 위치이니 기억해두세요.

5. 테이블 뷰에 셀을 등록한다.

  • 현재 테이블뷰는 ViewController에 있습니다. 그리고 셀은 그냥 따로 있습니다. 이 두 개의 객체가 서로 만날 교점은 없는 상태입니다.
  • 그러므로 두 교점을 만들어줄겁니다. 아래 코드를 ViewDidLoad에 추가해줍니다.
tableView.register(UINib(nibName: "PhoneBookTableViewCell", bundle: nil), forCellReuseIdentifier: "PhoneBookTableViewCell")
  • 이 코드에서 forCellReuseIdentifier 를 제가 설정하지 않은 것 같네요.
  • 다시 PhoneBookTableViewCell.xib 파일로 가서 identifier를 설정해줍니다. (아래그림참조)

    자 여기까지하면 UI 배치는 완료되었습니다.

Controller

컨트롤러는 말그대로 “제어” 입니다.
Model의 데이터를 가져올 예정이고, 그 데이터를 View에 전달해주는 역할을 하면 됩니다.

Model의 데이터를 Controller로

ViewController.swift 에 아래 코드를 멤버변수로 추가해줍니다.

let dataModel = TableViewModel()

그리고 이전에 UITableViewDataSoure 메소드를 다음과 같이 수정합니다.

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataModel.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "PhoneBookTableViewCell", for: indexPath) as? PhoneBookTableViewCell else { return UITableViewCell() }
        
        return cell
    }
}

자 거의다 왔습니다.

이제 dummy data를 넣어보도록 하겠습니다.
viewDidLoad 에 데이터를 추가하는 코드를 추가합니다.

override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
        
        tableView.register(UINib(nibName: "PhoneBookTableViewCell", bundle: nil), forCellReuseIdentifier: "PhoneBookTableViewCell")
        
        dataModel.addItem(TableViewModel.CellData(name: "김우노", cellPhoneNumber: 1))
        dataModel.addItem(TableViewModel.CellData(name: "이우노", cellPhoneNumber: 2))
        dataModel.addItem(TableViewModel.CellData(name: "박우노", cellPhoneNumber: 3))
        dataModel.addItem(TableViewModel.CellData(name: "최우노", cellPhoneNumber: 4))
        dataModel.addItem(TableViewModel.CellData(name: "정우노", cellPhoneNumber: 5))
    }

그러고 실행하면 다음과 같은 화면이 나옵니다.

갯수는 맞게 들어갔는데, cell에 데이터가 전달되지 않았네요.
그러면 이제 셀에 데이터를 전달하기 위해서 “PhonebookTableViewCell.swift” 파일로 다시 이동합니다.

그리고 아래와 같이 메소드를 추가해줍니다.

class PhoneBookTableViewCell: UITableViewCell {

    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var cellPhoneNumberLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    public func updateUI(cellData: PeopleInfo) {
        let nameStr = cellData.name
        let phoneNumberInt = cellData.cellPhoneNumber
        
        self.nameLabel.text = nameStr
        self.cellPhoneNumberLabel.text = String(phoneNumberInt)
    }
}

이제 데이터를 받을 준비를 했으니

ViewController.swift 로 이동해서 “cellForRowsAt” 메소드에 아래와 같이 코드를 수정합니다.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "PhoneBookTableViewCell", for: indexPath) as? PhoneBookTableViewCell else { return UITableViewCell() }
        
        if let cellData = dataModel.itemAt(index: indexPath.row) {
            cell.updateUI(cellData: cellData)
        }
        
        return cell
    }

그리고 실행하면 다음처럼 나오실겁니다.

정리

MVC 패턴으로 UITableview를 구현해봤습니다. 꼭 이러한 구성으로 UITableView를 만들어야하는 것은 전혀 아닙니다. 더 좋고 짧은 코드가 있을 수 있습니다. 다만, 데이터를 어떻게 관리할 것인지, UI를 어떻게 업데이트 할 것인지에 대한 고민 그리고 그 목적을 달성하기만 하면 되는 겁니다.

부족한 글 끝까지 읽어주셔서 감사하고, 아래 깃헙링크에 프로젝트 전체 파일을 두겠습니다.
https://github.com/kipsong133/UITableViewExampleAppliedMVCPattern

참고자료


profile
iOS & Flutter

0개의 댓글