Combine의 @Published로 데이터를 자동 바인딩합니다. SwiftUI와 궁합이 최고이며, Command 패턴으로 Undo까지 지원합니다.
Factory Method — TaskItem 생성 로직을 캡슐화.
Command — ToggleCommand, DeleteCommand로 Undo 스택 구현.
import Foundation
import Combine
// MARK: - MVVM ViewModel (완성)
protocol TaskCommand {
func execute()
func undo()
}
@MainActor
final class TaskListViewModel: ObservableObject {
@Published var tasks: [TaskItem] = []
@Published var searchText = ""
@Published var filterPriority: TaskItem.Priority? = nil
@Published var filteredTasks: [TaskItem] = []
@Published var showAddSheet = false
private var commandHistory: [any TaskCommand] = []
private var cancellables = Set<AnyCancellable>()
private let storage = TaskStorage.shared
init() {
tasks = storage.loadAll()
setupFilterPipeline()
}
// Combine 파이프라인: 검색 + 필터를 자동으로 처리
private func setupFilterPipeline() {
$tasks.combineLatest($searchText, $filterPriority)
.map { tasks, search, priority in
tasks.filter { task in
let matchSearch = search.isEmpty ||
task.title.localizedCaseInsensitiveContains(search)
let matchPriority = priority == nil || task.priority == priority
return matchSearch && matchPriority
}
}
.assign(to: &$filteredTasks)
}
func addTask(title: String, priority: TaskItem.Priority = .medium) {
let task = TaskItem(title: title, priority: priority)
tasks.insert(task, at: 0)
storage.saveAll(tasks)
}
// Command 패턴: 액션을 객체로 캡슐화 → Undo 가능
func toggleTask(_ task: TaskItem) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
let cmd = ToggleCommand(viewModel: self, index: idx)
cmd.execute()
commandHistory.append(cmd)
}
func deleteTask(_ task: TaskItem) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
let cmd = DeleteCommand(viewModel: self, index: idx, task: task)
cmd.execute()
commandHistory.append(cmd)
}
func undo() {
guard let last = commandHistory.popLast() else { return }
last.undo()
}
var canUndo: Bool { !commandHistory.isEmpty }
// ---- Command 구현체 ----
final class ToggleCommand: TaskCommand {
let vm: TaskListViewModel; let index: Int
init(viewModel: TaskListViewModel, index: Int) { self.vm = viewModel; self.index = index }
func execute() { vm.tasks[index].isCompleted.toggle(); vm.storage.saveAll(vm.tasks) }
func undo() { execute() } // 토글은 역연산이 동일
}
final class DeleteCommand: TaskCommand {
let vm: TaskListViewModel; let index: Int; let task: TaskItem
init(viewModel: TaskListViewModel, index: Int, task: TaskItem) {
self.vm = viewModel; self.index = index; self.task = task
}
func execute() { vm.tasks.remove(at: index); vm.storage.saveAll(vm.tasks) }
func undo() { vm.tasks.insert(task, at: min(index, vm.tasks.count)); vm.storage.saveAll(vm.tasks) }
}
}@Published를 붙이면 Combine Publisher가 자동 생성됩니다. SwiftUI View는 @ObservedObject로 구독하여 값 변경 시 자동 리렌더링합니다.combineLatest로 여러 Publisher를 합치고 map으로 변환합니다. 검색 텍스트나 필터가 바뀔 때마다 자동으로 filteredTasks가 업데이트됩니다.execute() / undo()를 가진 객체로 캡슐화합니다. 히스토리 스택에 쌓아 두었다가 역순으로 실행하면 Undo가 됩니다.@MainActor를 클래스에 붙이면 모든 메서드가 자동으로 메인 스레드에서 실행됩니다.