Apple의 기본 패턴. 가장 직관적이지만 Controller가 비대해지는 "Massive VC" 문제를 직접 체감할 수 있습니다. 이후 아키텍처들이 이 문제를 어떻게 해결하는지 비교하는 출발점입니다.
Singleton — TaskStorage.shared로 전역 저장소 접근.
Delegate — UITableViewDelegate로 이벤트 위임.
import UIKit
// MARK: - MVC Start
// TODO: MVC 패턴으로 Task 목록 화면을 완성하세요.
// 주목: Controller가 얼마나 커지는지 느껴보세요!
class TaskListViewController: UITableViewController {
private var tasks: [TaskItem] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Tasks (MVC)"
setupUI()
loadTasks()
}
private func setupUI() {
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .add, target: self, action: #selector(addTapped))
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
private func loadTasks() {
// TODO: TaskStorage.shared에서 tasks를 로드하고 tableView를 리로드하세요
}
@objc private func addTapped() {
// TODO: UIAlertController로 새 Task 추가 UI를 만드세요
}
// MARK: - TableView DataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// TODO: tasks.count 반환
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
// TODO: task의 title, priority emoji, 완료 상태를 표시하세요
return cell
}
// MARK: - TableView Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// TODO: 완료 상태를 토글하세요
}
override func tableView(_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath) {
// TODO: 스와이프 삭제를 구현하세요
}
}import Foundation
// MARK: - Singleton 패턴 (완성)
final class TaskStorage {
static let shared = TaskStorage() // ← Singleton
private init() {}
private let key = "mvc_tasks"
private let defaults = UserDefaults.standard
func loadAll() -> [TaskItem] {
guard let data = defaults.data(forKey: key),
let tasks = try? JSONDecoder().decode([TaskItem].self, from: data)
else { return TaskItem.samples }
return tasks
}
func saveAll(_ tasks: [TaskItem]) {
if let data = try? JSONEncoder().encode(tasks) {
defaults.set(data, forKey: key)
}
}
func add(_ task: TaskItem) {
var tasks = loadAll(); tasks.insert(task, at: 0); saveAll(tasks)
}
func update(_ task: TaskItem) {
var tasks = loadAll()
if let idx = tasks.firstIndex(where: { $0.id == task.id }) { tasks[idx] = task }
saveAll(tasks)
}
func delete(id: UUID) {
var tasks = loadAll(); tasks.removeAll { $0.id == id }; saveAll(tasks)
}
}import UIKit
// MARK: - MVC Finish (완성본)
// 주목: Controller가 Model 접근 + View 설정 + 사용자 인터랙션을 모두 처리
class TaskListViewController: UITableViewController {
private var tasks: [TaskItem] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Tasks (MVC)"
setupUI()
loadTasks()
}
private func setupUI() {
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .add, target: self, action: #selector(addTapped))
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
private func loadTasks() {
tasks = TaskStorage.shared.loadAll()
tableView.reloadData()
}
@objc private func addTapped() {
let alert = UIAlertController(title: "New Task", message: nil, preferredStyle: .alert)
alert.addTextField { $0.placeholder = "Task title" }
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Add", style: .default) { [weak self] _ in
guard let title = alert.textFields?.first?.text, !title.isEmpty else { return }
TaskStorage.shared.add(TaskItem(title: title))
self?.loadTasks()
})
present(alert, animated: true)
}
override func tableView(_ tv: UITableView, numberOfRowsInSection s: Int) -> Int { tasks.count }
override func tableView(_ tv: UITableView, cellForRowAt ip: IndexPath) -> UITableViewCell {
let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip)
let task = tasks[ip.row]
cell.textLabel?.text = "\(task.priority.emoji) \(task.title)"
cell.textLabel?.textColor = task.isCompleted ? .secondaryLabel : .label
cell.accessoryType = task.isCompleted ? .checkmark : .none
return cell
}
override func tableView(_ tv: UITableView, didSelectRowAt ip: IndexPath) {
tv.deselectRow(at: ip, animated: true)
tasks[ip.row].isCompleted.toggle()
TaskStorage.shared.update(tasks[ip.row])
tv.reloadRows(at: [ip], with: .automatic)
}
override func tableView(_ tv: UITableView, commit style: UITableViewCell.EditingStyle, forRowAt ip: IndexPath) {
guard style == .delete else { return }
let id = tasks[ip.row].id
tasks.remove(at: ip.row)
TaskStorage.shared.delete(id: id)
tv.deleteRows(at: [ip], with: .fade)
}
}TaskStorage.shared는 앱 전체에서 하나의 인스턴스만 존재합니다. private init()으로 외부 생성을 막고, static let shared로 전역 접근점을 제공합니다.didSelectRowAt, numberOfRowsInSection 등이 Delegate 메서드입니다.