Swift: Create, update and delete data
전체 흐름
1. 디테일 뷰 만들기 (@Bindable로 편집 가능한 뷰)
↓
2. 네비게이션 연결 (목록 → 디테일)
↓
3. 데이터 추가 (toolbar + addFriend/addMovie)
↓
4. 데이터 삭제 (onDelete + deleteFriends/deleteMovies)
↓
5. 시트(Sheet)로 추가 경험 개선
↓
6. 시트에 Save/Cancel 버튼 추가
↓
7. isNew로 추가 vs 편집 상황 분기
@Bindable
SwiftData로 만든 모델(Friend, Movie 등)은 SwiftUI가 값 변화를 자동으로 감지할 수 있다. @Bindable은 이 모델의 특정 프로퍼티를 TextField 같은 입력 컨트롤에 직접 연결할 수 있게 해주는 키워드다.
예를 들어 friend.name이 바뀌면 화면도 자동으로 바뀌고, 반대로 TextField에 타이핑하면 friend.name도 자동으로 업데이트된다. 이게 바로 양방향 연결이다.
// 이렇게 선언하면
@Bindable var friend: Friend
// $를 붙여서 TextField에 연결할 수 있다
TextField("Name", text: $friend.name)
$를 붙이는 게 포인트인데, $가 없으면 그냥 값을 읽기만 하고, $가 있으면 읽고 쓰기 모두 가능한 양방향 연결이 된다.
참고로 @State는 뷰 안에서 직접 만든 일반 변수(String, Int 등)에 쓰고, @Bindable은 SwiftData 모델처럼 외부에서 받아온 객체에 쓴다.
Form
iOS 설정 앱처럼 생긴 편집 화면을 만들 때 쓰는 컨테이너다. 안에 TextField나 DatePicker 같은 입력 컨트롤을 넣으면 자동으로 정렬되고 배경색도 깔끔하게 처리된다.
Form {
TextField("Name", text: $friend.name)
.autocorrectionDisabled()
}
.autocorrectionDisabled()는 자동 수정 기능을 끄는 거다. 이름 같은 고유명사를 입력할 때 자동 수정이 엉뚱하게 바꿔버리는 걸 방지한다.
DatePicker
날짜나 시간을 선택하는 UI 컨트롤이다.
DatePicker("Release date", selection: $movie.releaseDate, displayedComponents: .date)
"Release date": 왼쪽에 표시되는 레이블selection:: 선택한 날짜를 저장할 변수 ($붙여서 양방향 연결)displayedComponents:: 날짜만 보여줄지, 시간만 보여줄지 결정
.date // 날짜만 (2024. 1. 1.)
.hourAndMinute // 시간만 (오전 10:30)
[.date, .hourAndMinute] // 날짜 + 시간 둘 다
Form 안에서 쓰면 레이블은 왼쪽, 선택 버튼은 오른쪽으로 자동 정렬된다.
NavigationSplitView & NavigationStack
NavigationSplitView
목록 화면과 디테일 화면을 함께 관리하는 컨테이너다. iPhone에서는 목록 → 디테일로 이동하는 방식으로 동작하고, iPad에서는 좌우로 나란히 보여준다.
NavigationSplitView {
// 왼쪽: 목록
List { ... }
} detail: {
// 오른쪽: 디테일 (아무것도 선택 안 했을 때 보이는 기본 화면)
Text("Select a friend")
}
NavigationStack
한 번에 하나의 화면만 보여주는 단순한 스택이다. 주로 미리보기나 시트 안에서 네비게이션 바(상단 타이틀, 뒤로가기 버튼 영역)를 표시할 때 감싸서 사용한다.
NavigationStack {
FriendDetail(friend: friend)
}
.navigationTitle
화면 상단에 타이틀을 표시한다. 반드시 NavigationStack이나 NavigationSplitView 안에 있어야 표시된다. 그냥 단독으로 쓰면 아무것도 안 보인다.
.navigationTitle("Friends")
.navigationBarTitleDisplayMode(.inline) // 작은 글씨로 가운데 표시
.navigationBarTitleDisplayMode(.large) // 큰 글씨로 표시 (기본값)
.toolbar & ToolbarItem
네비게이션 바에 버튼 같은 걸 추가할 때 쓴다. + 버튼이나 Edit 버튼을 추가하는 게 대표적인 예시다.
.toolbar {
ToolbarItem {
Button("Add friend", systemImage: "plus", action: addFriend)
}
}
systemImage: "plus": SF Symbols에서+아이콘을 가져옴action: addFriend: 버튼을 탭했을 때 실행할 함수
placement로 버튼 위치를 지정할 수 있다.
ToolbarItem(placement: .topBarTrailing) // 오른쪽 끝
ToolbarItem(placement: .topBarLeading) // 왼쪽 끝
ToolbarItem(placement: .confirmationAction) // Save 버튼 위치 (SwiftUI가 알아서 배치)
ToolbarItem(placement: .cancellationAction) // Cancel 버튼 위치 (SwiftUI가 알아서 배치)
.confirmationAction, .cancellationAction은 위치를 직접 지정하는 게 아니라 "이건 확인 버튼이야", "이건 취소 버튼이야"라고 의미만 알려주면 SwiftUI가 플랫폼에 맞는 위치에 자동으로 배치해준다.
EditButton
SwiftUI가 기본 제공하는 버튼으로 탭하면 편집 모드로 전환되고 버튼 텍스트도 Edit ↔ Done으로 자동으로 바뀐다.
ToolbarItem(placement: .topBarTrailing) {
EditButton()
}
편집 모드에서는 .onDelete로 추가된 삭제 버튼(빨간 동그라미)이 각 행에 표시된다.
ModelContext - insert & delete
SwiftData에서 데이터를 추가하거나 삭제할 때 쓰는 객체다. @Environment로 가져온다.
@Environment(\.modelContext) private var context
@Environment는 SwiftUI가 앱 전체에서 공유하는 값들을 꺼내쓸 때 사용하는 키워드다. modelContext는 SwiftData의 데이터베이스 연결 통로라고 보면 된다.
// 추가
context.insert(Friend(name: ""))
// 삭제
context.delete(friend)
insert()에 새로 만든 객체를 넘기면 SwiftData가 자동으로 저장한다. delete()에 삭제할 객체를 넘기면 SwiftData가 자동으로 제거한다.
.onDelete
ForEach에 붙이면 각 행을 왼쪽으로 스와이프했을 때 Delete 버튼이 나타나는 기능이 자동으로 생긴다.
ForEach(friends) { friend in
NavigationLink(friend.name) { ... }
}
.onDelete(perform: deleteFriends(at:))
삭제 메서드는 IndexSet을 받아야 한다. IndexSet은 삭제할 항목들의 위치(인덱스) 번호를 담은 집합이다.
private func deleteFriends(at offsets: IndexSet) {
for index in offsets {
context.delete(friends[index]) // 인덱스로 배열에서 항목 꺼내서 삭제
}
}
함수 참조로 전달할 때 파라미터가 있으면 파라미터 이름까지 같이 써줘야 한다.
// 파라미터 없는 함수
action: addFriend
// 파라미터 있는 함수 → 파라미터 이름 포함
.onDelete(perform: deleteFriends(at:))
Optional과 !
값이 있을 수도 있고 없을 수도 있는 변수를 선언할 때 타입 뒤에 ?를 붙인다.
var newFriend: Friend? // Friend 객체가 있을 수도, nil일 수도 있음
Swift는 구조체를 만들 때 ?가 붙은 프로퍼티의 초기값을 자동으로 nil로 설정한다.
!는 옵셔널을 강제로 열어서 안의 값을 꺼내는 연산자다. 값이 없는데(nil) 강제로 열면 앱이 즉시 크래시 나기 때문에 값이 반드시 있다고 확신할 수 있는 상황에서만 써야 한다.
Friend.sampleData.first! // 샘플 데이터가 반드시 있다고 확신할 수 있는 미리보기 코드에서만 사용
.sheet
화면 아래에서 올라오는 모달 팝업이다. 옵셔널 프로퍼티의 값이 nil이냐 아니냐로 시트를 열고 닫는다.
@State private var newFriend: Friend?
.sheet(item: $newFriend) { friend in
FriendDetail(friend: friend, isNew: true)
}
newFriend가nil→ 시트 닫힘newFriend에 값이 생기면 → 시트 자동으로 열림- 사용자가 시트를 드래그해서 닫으면 →
newFriend가 자동으로nil로 변경
$newFriend처럼 $를 붙여서 바인딩으로 넘기는 이유는, 시트 자신이 스스로 닫힐 때 newFriend를 nil로 바꿀 수 있어야 하기 때문이다.
@Environment(.dismiss)
현재 화면(시트, 모달 등)을 닫는 동작을 가져오는 키워드다.
@Environment(\.dismiss) private var dismiss
dismiss()를 호출하면 현재 화면이 닫힌다. 어떤 방식으로 화면이 열렸는지(시트, 네비게이션 등) 상관없이 동작한다.
Button("Save") {
dismiss() // 시트 닫기
}
Button("Cancel") {
context.delete(friend) // 추가했던 데이터 삭제
dismiss() // 시트 닫기
}
Cancel을 눌렀을 때 context.delete()를 먼저 호출하는 이유는, + 버튼을 누른 시점에 이미 context.insert()로 데이터가 추가된 상태이기 때문이다. 취소할 때 직접 지워주지 않으면 빈 이름의 데이터가 그대로 남는다.
.interactiveDismissDisabled
시트를 아래로 드래그해서 닫는 걸 막는 수정자다.
.sheet(item: $newFriend) { friend in
FriendDetail(friend: friend, isNew: true)
.interactiveDismissDisabled()
}
드래그로 닫으면 Save를 원했는지 Cancel을 원했는지 알 수 없기 때문에 막고, 반드시 Save 또는 Cancel 버튼으로만 닫도록 강제한다.
isNew와 커스텀 init
같은 뷰(FriendDetail)를 두 가지 상황에서 사용한다.
- 목록에서 탭해서 편집할 때 → Save/Cancel 버튼 불필요
+버튼으로 새로 추가할 때 → Save/Cancel 버튼 필요
isNew라는 Bool 프로퍼티로 두 상황을 구분한다.
let isNew: Bool
init(friend: Friend, isNew: Bool = false) {
self.friend = friend
self.isNew = isNew
}
isNew: Bool = false처럼 기본값을 주면 기존에 FriendDetail(friend: friend)로 쓰던 코드를 전혀 수정하지 않아도 된다.
커스텀 init을 직접 쓰는 이유는 Xcode 자동완성으로 만들면 @Environment 프로퍼티인 dismiss와 context까지 파라미터로 포함해버리기 때문이다. 이 둘은 SwiftUI가 자동으로 주입해주는 값이라 init에서 직접 받으면 안 된다.
.toolbar {
if isNew {
ToolbarItem(placement: .confirmationAction) {
Button("Save") { dismiss() }
}
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
context.delete(friend)
dismiss()
}
}
}
}
isNew가 true일 때만 Save/Cancel 버튼을 보여주고, 편집 모드일 때는 버튼 없이 변경 사항이 즉시 자동 저장된다.
.modelContainer
앱 전체에서 SwiftData를 사용할 수 있도록 데이터베이스 환경을 설정하는 수정자다.
@main
struct FriendsFavoriteMoviesApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Movie.self, Friend.self])
}
}
import SwiftData 없이도 사용 가능하다. 이걸 추가하기 전까지는 미리보기에서만 데이터가 동작하고 실제 시뮬레이터에서는 데이터가 없다.
2026년 5월 30일 PM 2:59
로그인 필요