본문 바로가기
AI 해보기/딸깍! 일정관리 앱 만들어보기

3. 직접 새 일정 추가하기

by yoondoo 2026. 3. 20.
728x90

0. 이전

  • 고정된 일정 수
    • 이번에 '+' 아이콘을 사용해 일정을 추가할 수 있는 기능을 만들어보자

1. .toolbar = 상단 버튼 영역

앱 상단에 버튼을 배치하는 영역

캘린더 앱의 '+'버튼 처럼

우리도 상단 오른쪽에 배치할 예정

.toolbar {
	ToolbarItem(placement:
      .navigationBarTrailing) {
      Button {
      	showingAddEbent = true
      } label: {
      	Image(systemName: "plus")
      }
}
  • .toolbar는 NavigationStack 안에서만 동작
  • placement: .navigationBarTrailing = 오른쪽 상당 배치
  • 버튼 탭 -> showingAddEvent = true -> Sheet 열림

2. 입력화면 띄우기 .sheet (모달)

  • 화면 아래에서 위로 슬라이드 되며 나타남
  • 기존 화면 위에 덮어 씌우는 형태
  • isPresented 값이 true가 되면 열림, 닫으면 자동으로 false
@State var showingAddEvent = false

// NavigationStack 안에서
.sheet(isPresented:
		$showingAddEvent) {
    AddEventView(
   		events: $events
    )
}

.sheet 동작 흐름

  1. + 버튼
  2. showingAddEvent = true
  3. Sheet 올라옴(AddEventView)
  4. 저장 or 취소 후 화면 닫힘, showingAddEvent = false

3. TextField = 텍스트 입력 칸

  • 사용자가 키보드로 텍스트를 입려하는 칸
@State var title = ""
@State var memo = ""

TextField("제목을 입력하세요",
			text: $title)
TextField("메모을 입력하세요",
			text: $memo)

 

제목이 비어있으면 저장 버튼 비활성화!

  • .disabled(title.isEmpty)

Toggle 사용 (하루 종일 옵션)

  • Toggle("하루종일", isOn: $isAllDay)
  • 켜면 true, 끄면 false

4. DatePicker

  • 캘린더 형태로 날짜/시간을 선택하는 UI
  • 탭하면 캘린더가 펼쳐짐
  • 날짜와 시간 모두 선택 가능
  • @State var date = Date()로 저장
@State var date = Date()

DatePicker("날짜",
	selecetion: $date,
    displayedComponents:
      [.date, .hourAndMinute]
)

// .date = 날짜만
// .hourAndMinute = 시간만
// 둘 다 = 날짜 + 시간

5. 데이터 추가하기 @Binding으로 목록에 추가!

  • @Binding이란 부모 데이터를 자식이 수정할 수 있게 해주는 연결고리!

// AddEventView 안에서
@Binding var events: [Event]

func saveEvent() {
	let newEvent = Event(
    	title: title,
        date: date,
        memo: memo,
        isAllDay: isAllDay
    )
    events.append(newEvent)
    dismiss() // sheet 닫기
}

6.  프롬프트 작성

일정관리 앱을 이어서 개발할게.

추가할 기능:
1. 상단에 + 버튼이 있다
2. + 버튼을 탭하면 일정 추가 화면이 나타난다
3. 제목, 날짜, 메모를 입력할 수 있다
4. 하루 종일 토글이 있다
5. 저장 버튼을 누르면 목록에 추가된다
6. 취소 버튼을 누르면 추가하지 않고 닫힌다
7. 제목이 비어있으면 저장 버튼이 비활성화된다

전체 코드를 하나의 파일로 다시 작성해줘.
import SwiftUI

// MARK: - 모델

struct Event: Identifiable {
    let id = UUID()
    var title: String
    var date: Date
    var memo: String
    var isAllDay: Bool
}

// MARK: - 샘플 데이터

extension Event {
    static func 샘플만들기() -> [Event] {
        let 날짜형식 = DateFormatter()
        날짜형식.dateFormat = "yyyy-MM-dd"

        let 시간형식 = DateFormatter()
        시간형식.dateFormat = "yyyy-MM-dd HH:mm"

        return [
            Event(title: "팀 회의",       date: 시간형식.date(from: "2026-03-18 14:00")!, memo: "분기 목표 점검",      isAllDay: false),
            Event(title: "치과 예약",      date: 시간형식.date(from: "2026-03-20 10:30")!, memo: "정기 검진",          isAllDay: false),
            Event(title: "친구 생일 파티",  date: 날짜형식.date(from: "2026-03-22")!,        memo: "선물 준비하기",      isAllDay: true),
            Event(title: "프로젝트 마감",   date: 시간형식.date(from: "2026-03-25 18:00")!, memo: "최종 보고서 제출",    isAllDay: false),
            Event(title: "가족 저녁 식사",  date: 날짜형식.date(from: "2026-03-30")!,        memo: "레스토랑 예약 완료",  isAllDay: true),
        ]
    }
}

// MARK: - 날짜 표시 헬퍼

struct DateLabel: View {
    let date: Date
    let isAllDay: Bool

    var body: some View {
        if isAllDay {
            Text(date, format: .dateTime.year().month().day()) + Text(" (하루 종일)")
        } else {
            Text(date, format: .dateTime.year().month().day().hour().minute())
        }
    }
}

// MARK: - 목록 행

struct EventRow: View {
    let event: Event

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(event.title)
                .font(.headline)
            DateLabel(date: event.date, isAllDay: event.isAllDay)
                .font(.subheadline)
                .foregroundColor(.secondary)
        }
        .padding(.vertical, 4)
    }
}

// MARK: - 상세 화면

struct EventDetailView: View {
    let event: Event

    var body: some View {
        List {
            Section("제목") {
                Text(event.title)
                    .font(.title3.bold())
            }

            Section("날짜") {
                DateLabel(date: event.date, isAllDay: event.isAllDay)
            }

            Section("메모") {
                Text(event.memo.isEmpty ? "없음" : event.memo)
                    .foregroundColor(event.memo.isEmpty ? .secondary : .primary)
            }
        }
        .navigationTitle("일정 상세")
        .navigationBarTitleDisplayMode(.inline)
    }
}

// MARK: - 일정 추가 화면 (@Binding 사용)

struct AddEventView: View {
    @Environment(\.dismiss) private var dismiss

    /// 부모 뷰의 일정 배열을 직접 참조하는 바인딩
    @Binding var events: [Event]

    // 입력 폼 상태
    @State private var title = ""
    @State private var date = Date()
    @State private var memo = ""
    @State private var isAllDay = false

    /// 제목이 비어있는지 확인
    private var 제목비어있음: Bool {
        title.trimmingCharacters(in: .whitespaces).isEmpty
    }

    var body: some View {
        NavigationView {
            Form {
                Section("제목") {
                    TextField("일정 제목을 입력하세요", text: $title)
                }

                Section("날짜") {
                    Toggle("하루 종일", isOn: $isAllDay)

                    DatePicker(
                        "날짜",
                        selection: $date,
                        displayedComponents: isAllDay ? [.date] : [.date, .hourAndMinute]
                    )
                }

                Section("메모") {
                    TextField("메모를 입력하세요", text: $memo, axis: .vertical)
                        .lineLimit(3...6)
                }
            }
            .navigationTitle("새 일정")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("취소") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("저장") {
                        // @Binding을 통해 부모의 배열에 직접 추가
                        let 새일정 = Event(
                            title: title.trimmingCharacters(in: .whitespaces),
                            date: date,
                            memo: memo.trimmingCharacters(in: .whitespaces),
                            isAllDay: isAllDay
                        )
                        events.append(새일정)
                        dismiss()
                    }
                    .disabled(제목비어있음)
                }
            }
        }
    }
}

// MARK: - 목록 화면

struct ContentView: View {
    @State private var events = Event.샘플만들기()
    @State private var showingAddSheet = false

    var body: some View {
        NavigationView {
            List(events) { event in
                NavigationLink(destination: EventDetailView(event: event)) {
                    EventRow(event: event)
                }
            }
            .navigationTitle("내 일정")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: { showingAddSheet = true }) {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $showingAddSheet) {
                // $events 바인딩을 자식 뷰에 전달
                AddEventView(events: $events)
            }
        }
    }
}

// MARK: - 앱 진입점

@main
struct MyScheduleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

7. 실행 체크 

  • [ㅇ] + 버튼을 눌러서 추가 화면을 열었다
  • [ㅇ] 제목, 날짜, 메모를 직접 입력했다
  • [ㅇ] 저장 버튼을 눌러서 목록에 추가했다
  • [ㅇ] 취소 버튼도 눌러봤다
  • [ㅇ] 실제로 내 일정을 3개 이상 추가했다

 

반응형

댓글