본문 바로가기
SwiftUI 독학으로 기본기 익히기

05. 새로운 할 일 추가 기능 구현

by daco2020 2025. 10. 30.

지난 글(투두리스트 화면과 내비게이션 구현하기)에 이어서 사용자가 직접 새로운 할 일을 추가하는 기능을 완성해 보겠습니다.

 

이번 글에서는 사용자가 '+' 버튼을 누르면 새로운 입력창이 나타나고, 내용을 입력 후 저장하면 리스트에 추가합니다. 이 과정에서 화면을 띄우는 .sheet, 사용자 입력을 받는 TextField, 그리고 네비게이션 바에 버튼을 추가하는 .toolbar를 배워보겠습니다.

 

 


 

 

 

Part 2 : 새로운 할 일 추가 기능 구현

이번에는 ContentView 외에 할 일을 추가하는 UI를 위한 새로운 AddItemView를 만들겠습니다.

 

ContentView 파일의 전체 코드를 아래 내용으로 교체해 주세요. 새로 추가된 부분과 주석을 특히 주의 깊게 보면서 타이핑해 보세요.

import SwiftUI

// 데이터 모델은 이전과 동일합니다.
struct TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
}

// 할 일을 추가하는 화면을 위한 새로운 View
struct AddItemView: View {
    // 1. 사용자가 입력하는 텍스트를 저장하기 위한 @State 변수
    @State private var newItemTitle: String = ""

    // 2. ContentView로부터 "할 일을 추가하는 기능" 자체를 전달받기 위한 변수
    // (TodoItem) -> Void 는 "TodoItem 하나를 받아서 아무것도 반환하지 않는 함수" 라는 뜻입니다.
    let onAdd: (TodoItem) -> Void

    // 3. 이 화면을 스스로 닫기 위한 기능
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                // 4. 사용자가 할 일 내용을 입력하는 텍스트 필드
                TextField("새로운 할 일을 입력하세요", text: $newItemTitle)
                    .textFieldStyle(.roundedBorder)
                    .padding()

                Button(action: {
                    // 5. 저장 버튼 로직
                    // 입력된 내용이 비어있지 않다면
                    if !newItemTitle.isEmpty {
                        // 새로운 TodoItem을 만들고
                        let newItem = TodoItem(title: newItemTitle)
                        // ContentView에서 전달받은 onAdd 함수를 실행하여 데이터를 넘겨줍니다.
                        onAdd(newItem)
                        // 마지막으로, 현재 창을 닫습니다.
                        dismiss()
                    }
                }) {
                    Text("저장하기")
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .padding(.horizontal)

                Spacer() // 남은 공간을 모두 차지하여 UI를 위로 밀어 올립니다.
            }
            .navigationTitle("새로운 할 일 추가")
        }
    }
}


struct ContentView: View {
    @State private var todoItems: [TodoItem] = []

    // 6. '새 할 일 추가' 화면을 띄울지 말지 결정하는 @State 변수
    @State private var isShowingAddItemView: Bool = false

    var body: some View {
        NavigationStack {
            List { // 7. ForEach와 함께 사용하면 삭제 기능을 쉽게 추가할 수 있습니다.
                ForEach(todoItems) { item in
                    NavigationLink(destination: TodoDetailView(item: item)) {
                        HStack {
                            Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundStyle(item.isCompleted ? .green : .gray)
                            Text(item.title)
                        }
                    }
                }
                .onDelete(perform: deleteItem) // 8. 리스트에서 스와이프하여 삭제하는 기능
            }
            .navigationTitle("나의 할 일 목록")
            .toolbar {
                // 9. 네비게이션 바 오른쪽에 '+' 버튼을 추가합니다.
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        // 이 버튼을 누르면 isShowingAddItemView가 true로 바뀝니다.
                        isShowingAddItemView = true
                    }) {
                        Image(systemName: "plus")
                    }
                }
            }
            // 10. isShowingAddItemView가 true가 되면, sheet를 사용해 AddItemView를 화면에 띄웁니다.
            .sheet(isPresented: $isShowingAddItemView) {
                AddItemView { newItem in
                    // AddItemView에서 onAdd(newItem)을 호출하면 이 코드가 실행됩니다.
                    // 전달받은 newItem을 todoItems 배열의 맨 앞에 추가합니다.
                    todoItems.insert(newItem, at: 0)
                }
            }
        }
    }

    // 할 일을 삭제하는 함수
    func deleteItem(at offsets: IndexSet) {
        todoItems.remove(atOffsets: offsets)
    }
}


// 상세 뷰는 이전과 동일합니다.
struct TodoDetailView: View {
    let item: TodoItem

    var body: some View {
        VStack {
            Text(item.title)
                .font(.largeTitle)
            Text(item.isCompleted ? "완료됨" : "미완료")
                .foregroundStyle(item.isCompleted ? .green : .red)
        }
        .navigationTitle("상세 보기")
    }
}

 

 

핵심 코드에 대한 설명

 

 

1. @State private var isShowingAddItemView: Bool = false

 

AddItemView라는 창이 지금 화면에 보여지고 있는지 아닌지를 기억하는 스위치 역할의 변수입니다. 처음엔 false(안 보임) 상태입니다.

 

 

2. .toolbar { ... }

 

NavigationStack 안에서 사용되며, 네비게이션 바(상단 제목 영역)에 버튼 같은 UI 요소를 추가할 수 있게 해 줍니다. 우리는 오른쪽(navigationBarTrailing)에 '+' 모양의 Button을 추가했습니다.

 

 

3. .sheet(isPresented: $...) { ... }

 

SwiftUI에서 아래에서 위로 스르륵 올라오는 형태의 새 창(Sheet)을 띄울 때 사용합니다.

 

'isPresented: $isShowingAddItemView'는 "이 sheet는 $isShowingAddItemView 변수의 값에 따라 보여지거나 사라질 거야" 라는 뜻입니다. 

 

'$'는 이 변수와 sheet의 상태를 '연결(Binding)'한다는 의미입니다. isShowingAddItemView가 true가 되면 sheet가 올라오고, 사용자가 sheet를 쓸어내려 닫으면 다시 false가 됩니다.

 

'{ ... }'는 sheet가 올라올 때 어떤 화면을 보여줄지 그 내용을 넣는 곳입니다. 우리는 AddItemView를 보여주도록 했습니다.

 

 

4. AddItemView와 데이터 전달 방식

 

ContentView가 AddItemView를 띄울 때, onAdd: { newItem in ... } 이라는 코드 뭉치(클로저)를 전달합니다. 이것은 "나중에 '저장하기' 버튼을 눌러서 newItem이 생기면, 이 코드 뭉치를 실행시켜줘!" 라는 일종의 '예정된 명령'을 주는 것과 같습니다. 흔히 콜백 함수라고도 부르죠.

 

AddItemView는 이 onAdd 변수를 잘 가지고 있다가, '저장하기' 버튼이 눌리면 onAdd(newItem) 코드를 통해 ContentView에게 "여기 새로운 아이템이야!" 라고 보고(데이터 전달)합니다.

 

이 방식을 통해 자식 뷰(AddItemView)가 부모 뷰(ContentView)의 데이터를 변경할 수 있습니다.

 

 

5. .onDelete(perform: deleteItem)

 

List 안의 ForEach에 붙여서 사용하는 편리한 기능입니다. 리스트의 항목을 왼쪽으로 스와이프하면 '삭제' 버튼이 나타나게 해 줍니다. 삭제 버튼을 누르면 deleteItem 함수가 실행되어 todoItems 배열에서 해당 항목을 제거합니다. @State 변수가 변경되었으므로 리스트는 자동으로 새로고침됩니다.

 

 


 

 

이제 앱을 실행하고 확인해보세요.

 

실제 동작하는 모습을 녹화한 영상입니다.

 

 

- 시뮬레이터(Command + R)를 실행해서 오른쪽 위에 생긴 '+' 버튼을 눌러보세요.

- '새로운 할 일 추가' 창이 아래에서 위로 스르륵 올라오는 것을 확인합니다.

- 텍스트 필드에 새로운 할 일을 입력하고 '저장하기' 버튼을 누릅니다.

- 창이 닫히면서 방금 입력한 할 일이 목록의 맨 위에 추가되는 것을 확인하세요.

- 추가된 항목을 왼쪽으로 스와이프 하여 삭제 기능도 테스트해 보세요.

 

 

이렇게 사용자로부터 입력을 받아 데이터를 생성(Create)하고, 목록으로 읽고(Read), 삭제(Delete)까지 할 수 있는, 투두리스트 앱을 만들었습니다. 

 

하지만 아직 할 일을 완료처리할 수 있는 수정(Update) 기능이 없는데요. 다음 글에서는 투두리스트 앱의 마지막 파트로 완료처리 기능을 구현해 보겠습니다.