지난 글(새로운 할 일 추가 기능 구현)에서 우리는 할 일을 생성(Create), 목록으로 읽고(Read), 삭제(Delete)하는 것까지 구현했습니다.
하지만 투두리스트에서 가장 중요한 기능인 수정(Update) 기능, 즉 완료 상태를 변경하는 기능이 빠져있습니다. 이번 글에서는 할 일을 완료 처리하고, 한 번 더 선택하면 미완료 처리로 되돌리는 기능을 구현해보겠습니다.
Part 3 : 할 일 완료 처리 기능 구현하기
ContentView 부분을 아래와 같이 수정하겠습니다. 새롭게 추가되고 변경된 부분을 중심으로 코드를 살펴보세요. (AddItemView나 TodoDetailView 등 다른 부분은 그대로 두시면 됩니다)
struct ContentView: View {
@State private var todoItems: [TodoItem] = []
@State private var isShowingAddItemView: Bool = false
var body: some View {
NavigationStack {
List {
ForEach(todoItems) { item in
NavigationLink(destination: TodoDetailView(item: item)) {
HStack {
// 1. Image에 탭 제스처를 추가합니다.
Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(item.isCompleted ? .green : .gray)
.onTapGesture {
// 2. 이미지를 탭하면 이 코드가 실행됩니다.
toggleCompletion(for: item)
}
Text(item.title)
// 3. 완료된 항목은 취소선을 그어줍니다.
.strikethrough(item.isCompleted, color: .gray)
.foregroundStyle(item.isCompleted ? .gray : .primary)
}
}
}
.onDelete(perform: deleteItem)
}
.navigationTitle("나의 할 일 목록")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isShowingAddItemView = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isShowingAddItemView) {
AddItemView { newItem in
todoItems.insert(newItem, at: 0)
}
}
}
}
func deleteItem(offsets: IndexSet) {
todoItems.remove(atOffsets: offsets)
}
// 4. 할 일의 완료 상태를 변경하는 함수 (새로 추가!)
func toggleCompletion(for item: TodoItem) {
// `todoItems` 배열에서 내가 탭한 `item`과 동일한 id를 가진 항목이
// 몇 번째에 있는지 찾아냅니다.
if let index = todoItems.firstIndex(where: { $0.id == item.id }) {
// 해당 위치(index)에 있는 항목의 isCompleted 값을 반대로 뒤집습니다(toggle).
// (false -> true, true -> false)
todoItems[index].isCompleted.toggle()
}
}
}
핵심 코드에 대한 설명
1. .onTapGesture { ... }
Button처럼 UI 요소를 탭할 수 있게 만들어주는 기능입니다. 여기서는 circle Image를 탭했을 때 특정 코드를 실행시키고 싶어서 사용했습니다.
2. toggleCompletion(for: item)
onTapGesture 안에서 우리가 방금 탭한 item 을 알려주면서 toggleCompletion 함수를 호출합니다.
3. strikethrough(...) 와 .foregroundStyle(...)
strikethrough는 isCompleted가 true이면 텍스트에 취소선을 그어줍니다.
foregroundStyle는 isCompleted가 true이면 글자색을 회색으로 변경합니다.
이 부분은 없어도 되는 부분이지만 사용자 경험을 향상시키기 위한 디테일입니다.
4. toggleCompletion 함수의 작동 원리
if let index = todoItems.firstIndex(where: { $0.id == item.id }) 이 코드는 todoItems 배열 전체를 빠르게 스캔하여 id를 기준으로 아이템의 index(순서)를 변수에 저장합니다.
todoItems[index] 이 코드는 위에서 찾은 index를 사용해 @State 변수인 todoItems 배열의 원본 데이터에 직접 접근합니다. 그리고 .isCompleted.toggle()을 통해 Bool 값을 true는 false로, false는 true로 뒤집어 줍니다.
isCompleted 값이 변경되면 @State 변수의 내용이 변경되었기 때문에, SwiftUI는 이 변화를 감지하고 List 전체를 자동으로 새로 그려서 화면을 업데이트합니다.
여기서 이런 의문이 들수도 있습니다.
"왜 그냥 item.isCompleted.toggle() 이라고 하지 않고 index를 찾아 변경하나요?"
그 이유는 ForEach 안에서 사용하는 item은 todoItems 배열에 있는 원본 데이터를 복사(copy)해서 가져온 let(상수) 이기 때문입니다. 따라서 item의 값을 직접 바꾸려고 하면 "상수(let)는 바꿀 수 없다"는 에러가 발생합니다.
실제 데이터의 값을 바꾸려면 원본 데이터에 직접 접근해야 합니다. toggleCompletion 함수는 바로 이 동작을 수행합니다.
투두리스트 앱을 완성했습니다!
이제 시뮬레이터를 다시 실행(Command + R)하고, 할 일 목록의 동그라미 아이콘이나 체크 표시된 아이콘을 직접 탭해 보세요.

- 동그라미가 체크 표시로 바뀌고, 글자에 취소선이 그어지며 회색으로 변하는 것을 볼 수 있습니다.
- 다시 탭하면 원래 상태로 돌아옵니다.
여기까지 SwiftUI로 데이터를 생성(C), 조회(R), 수정(U), 삭제(D)하는 모든 기능을 갖춘 앱을 만들어 보았습니다.
'SwiftUI 독학으로 기본기 익히기' 카테고리의 다른 글
| 07. UserDefaults로 데이터를 저장하는 세 가지 방법 (1) | 2025.11.01 |
|---|---|
| 05. 새로운 할 일 추가 기능 구현 (0) | 2025.10.30 |
| 04. 투두리스트 화면과 내비게이션 구현하기 (0) | 2025.10.29 |
| 03. 로또 번호 생성기 앱 만들기 (0) | 2025.10.28 |
| 02. Text 수정하고 Image 추가하기 (0) | 2025.10.28 |