잔망스러운 신입의 주간 iOS 개발 일지 #4

이것, 저것 Feb 01, 2020

총평

MVC를 기반으로 한 Todo를 만들어서 MVVM으로 변경하는 간단한 연습을 진행 중 입니다. 1~3주차까지 배웠던 내용을 정리하는 시간을 가져보고 있습니다. TableView에 국한된 이야기지만, TableView를 기반으로 iOS 개발에 대한 간단한 접근을 시도해보았습니다. 이 글을 읽고계신 저와 같은 초보에게도 도움이 되었으면 합니다.

MVC?

일반적으로 대부분의 웹 프레임워크는 MVC 패턴을 차용합니다. 아래 그림에서 확인할 수 있듯이 View가 Model에 의존하고 있다는 점이 가장 큰 문제로 지적되었습니다.

초기 MVC

초기 MVC 이후 모바일에서 사용하는 MVC는 View와 Model이 Controller에 의존하는 형태의 MVC로 개선하여 사용됩니다. View와 Model의 의존관계를 대신해서 Controller가 View와 Model을 관리하는 제왕적 MVC 형태가 되었습니다. 이런 상황에선 당연하게도 Controller에 너무 많은 로직과 코드가 집중되게 됩니다.

중앙집중식 MVC

이번주는 Todo 예제를 MVC 패턴으로 연습하겠습니다. 코드 자체는 매우 짧은 코드로 설명하도록 하겠습니다.

MVC의 시작 'M'

간단한 Todo 예제를 위해서 모델은 간략하게 작성해보도록 하겠습니다. 모델의 최소한으로 작성하도록 하겠습니다. 잘 만들어진 Todo 앱(대표적인게 마이크로소프트의 To-Do)에 비해서 한참 모자라지만, 예제로 사용하기엔 차고 넘칩니다. Swift의 관례에 따라 시작은 구조체로 하겠습니다. 아마 예제의 특성상 class로 변경할 일은 없을 듯 합니다.


//
//  Task.swift
//  ToDoList-MVC
//
//  Created by Sangkon Han on 2020/02/04.
//  Copyright © 2020 Sangkon Han. All rights reserved.
//

import Foundation

struct Task {
    var id:Int
    var title:String
    var status:Bool
}

typealias Todo = [Task]

View와 Controller

TableViw의 경우 View와 Controller를 분리해서 만들 수 있지만, 저는 UITableViewController를 사용하도록 하겠습니다. 화면에 간단한 TableView를 출력하는데 UITableViewController 정도면 충분합니다.

//
//  ToDoController.swift
//  ToDoList-MVC
//
//  Created by Sangkon Han on 2020/02/04.
//  Copyright © 2020 Sangkon Han. All rights reserved.
//

import UIKit

class ToDoController: UITableViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()        
    }

}

Model과 Controller를 연결

UITableViewController에 모델을 연결하도록 하겠습니다. 그리고 예제 데이터를 추가해서 TableView에 샘플 데이터를 출력하도록 하겠습니다.

// 더미 데이터 생성
class ToDoController: UITableViewController {
    
    var listData:[Task] = [Task]()
    
    override func viewDidLoad() {
        super.viewDidLoad()        
        listData = [Task(id: 1, title: "Todo1", status: false),
                    Task(id: 2, title: "Todo2", status: false),
                    Task(id: 3, title: "Todo3", status: false),
                    Task(id: 4, title: "Todo4", status: true)]        
    }
}

// 화면에 데이터 출력
extension ToDoController {
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return listData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Defaults", for: indexPath)
        cell.textLabel?.text = listData[indexPath.row].title
        return cell
    }
}

화면에 Todo1 ~ Todo4까지 잘 출력됨을 확인할 수 있습니다. 하지만 지금까지 진행된 내용은 Model의 내용을 TableView에 출력하기 위한 기초적인 코드입니다. 모델의 특성에 맞게 View를 조금 더 손보도록 하겠습니다.

Controller과 View, TableViewCell을 사용한 사용자 View

UITableViewCell을 사용해서 체크박스와 입력창을 만들고, cell을 수정하면 데이터 모델을 반영한 View를 맏들 수 있습니다. 참고로 말씀드리자면, 회사에서 스토리보드를 전혀 사용하지 않아서 저도 스토리보드를 전혀 사용할줄 모릅니다(학습의 대상이 아님 -_-;;). 그러니 모든 View는 코드로 작성하도록 하겠습니다.

class TaskTableViewCell: UITableViewCell {
    
    let taskCheckBox: UIButton = {
        let checkbox = UIButton()
        checkbox.backgroundColor = .green
        checkbox.layer.borderWidth = 1
        checkbox.layer.borderColor = UIColor.gray.cgColor
        checkbox.translatesAutoresizingMaskIntoConstraints = false
        return checkbox        
    }()
    
    let taskCheckTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Input ToDo"
        textField.backgroundColor = .white
        textField.layer.cornerRadius = 4
        textField.textColor = .lightGray
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()
        
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        selectionStyle = .none
        backgroundColor = .clear
        contentView.backgroundColor = .white
                
        addSubview(taskCheckTextField)
        taskCheckTextField.leftAnchor.constraint(equalTo: leftAnchor, constant: 16).isActive = true
        taskCheckTextField.topAnchor.constraint(equalTo: topAnchor, constant: 16).isActive = true
        taskCheckTextField.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
        taskCheckTextField.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16).isActive = true

        addSubview(taskCheckBox)
        taskCheckBox.rightAnchor.constraint(equalTo: rightAnchor, constant: -16).isActive = true
        taskCheckBox.widthAnchor.constraint(equalToConstant: 16).isActive = true
        taskCheckBox.heightAnchor.constraint(equalTo: taskCheckBox.widthAnchor).isActive = true
        taskCheckBox.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

중간정리

Model을 작성, Contorller를 사용해서 데이터 바인딩 후 View에 해당 값을 출력하였습니다. 일반적으로 MVC 패턴을 사용하면 해당 과정을 반드시 거치게 됩니다. 그렇다면 이제 다른 주제로 넘어가겠습니다. 기존의 데이터에 값을 추가하는 과정은 어떻게 진행되어야 할까요?

데이터 추가

데이터를 추가하기 위해선 데이터를 추가하는 기능이 필요합니다. 하지만 데이터를 추가하기 위해선 지금까지 작성한 진행방향과 역순으로 진행할 필요가 있습니다. 대부분의 데이터는!? View에 입력됩니다. iOS는 제왕적 MVC 모델을 사용하고 있기 때문에 View에서 Model로 접근할 수 없습니다. 초기 MVC 패턴이 가지고 있는 장점은 데이터를 입력하는 부분에서 확연하게 드러납니다. 그리고 초창기 MVC 패턴이 가진 철학이 확인할 수 있습니다. 당연히 사용자는 View를 통해서 데이터를 입력합니다. 그러니 입력된 데이터를 Model에 반영하고, Model에 반영된 데이터를 Controller에서 View로 전달하면 됩니다. 하지만 제왕적 MVC 패턴은 View가 Model에 직접 데이터를 추가할 수 없습니다.

따라서 데이터의 흐름은 View → Controller → Model 순서로 진행됩니다. 그리고 Model에 데이터가 추가되면 화면을 다시 그려야(draw)해야 됩니다. 이제 시작해보겠습니다.

입력창 만들기

아주 간단하게 버튼을 하나 만들어보도록 하겠습니다. 앞서 소개한 코드랑 유사합니다. 스토리보드를 사용할줄 몰라서 코드로 대신합니다.


let addButton:UIButton = {
    let add = UIButton.init(type: .system)
    add.setTitle("Add", for: .normal)
    add.translatesAutoresizingMaskIntoConstraints = false
    return add
}()

...

해당 버튼을 TableView에 붙여보도록 하겠습니다. 네비게이션에 버튼을 '+'로 만드는 방법을 몰라서(!) 하단에 Add 버턴을 만들어보도록 하겠습니다. 코드가 길지만 결론은 하단에 버튼을 붙여라 입니다.

tableView.addSubview(addButton)
addButton.leftAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.leftAnchor).isActive = true
addButton.rightAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.rightAnchor).isActive = true
addButton.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor).isActive = true
addButton.widthAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.widthAnchor).isActive = true
addButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
addButton.addTarget(self, action: #selector(self.displayAddToDoDialog), for: .touchUpInside)

...

버튼을 붙였으니 데이터에 값을 추가하고, 화면을 다시 그리는 액션을 만들도록 하겠습니다. 코드가 복잡한건 View를 알람창을 사용해서 대체하고 있기 때문입니다. 결론적으로 아래와 같은 코드를 사용하면 화면에 어떤 값이 추가되는 것을 확인할 수 있습니다.

extension ToDoController {
    @objc func displayAddToDoDialog() -> Void {
        let alertController = UIAlertController(title: "Add ToDo", message: nil, preferredStyle: .alert)
        alertController.addTextField(configurationHandler: { textField in
            textField.clearButtonMode = UITextField.ViewMode.whileEditing
        })
        alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in
            guard let self = self, let inputTextField = alertController.textFields?.first else { return }
            guard let inputText = inputTextField.text else { return }
            self.add(item: Task(id: self.listData.count+1, title: inputText, status: false))
        }))
        alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        self.present(alertController, animated: true, completion: nil)
    }
    
    func add( item: Task ) {
        listData.append(item)
        tableView.reloadData()
    }
}

이번주는 파이썬 스터디가 있어서 이 정도에서 멈추도록 하겠습니다. 대충 이렇게 돌아간다 생각하시면 됩니다. 그리고 이렇게 바보같이 만들기 보다는 당연하게도 '델리게이트 패턴'을 적극적으로 활용하시면 Add 버튼을 별도의 컴포넌트로 만들수 있습니다. 아직 개발 시작한지 4주차(어.. 1달!)라서 고급 기능을 사용할 수 없어서 대충 코드를 구겨넣었습니다.

다음주는

여튼 데이터가 어디서 어디로 가는지는 감을 잡았으니 다음주(5주차에는) MVVM으로 Todo를 만들어보도록 하겠습니다. 그런데 회사에서 SwiftUI를 사용하지 않는다고 하니(버그가 많다고 하네욤!), 전 SwiftUI를 사용해보도록 하겠습니다. 하하하하!

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.