Swift: SwiftData 기초 사용법
1. @Model 매크로
@Model은 SwiftData가 제공하는 매크로로, 일반 Swift class를 SwiftData가 관리하는 영구 저장 모델로 변환해준다. 매크로는 겉으로 보이지 않는 코드를 자동으로 추가해주는 기능이다.
반드시 class여야 하는 이유: class는 인스턴스마다 고유한 identity(참조)를 가지기 때문에, SwiftData가 앱 전체에서 동일한 객체를 추적하고 공유할 수 있다. struct는 값을 복사해서 전달하기 때문에 이것이 불가능하다.
// ❌ struct는 @Model 사용 불가 → 오류 발생
@Model
struct Friend {
var name: String
}
// ✅ class에 @Model 적용
@Model
class Friend {
var name: String
var birthday: Date
init(name: String, birthday: Date) {
self.name = name
self.birthday = birthday
}
}
2. init (이니셜라이저)
이니셜라이저는 객체를 처음 만들 때 모든 프로퍼티에 초기값을 할당하는 함수다. 다른 언어의 constructor와 완전히 동일한 개념이다.
struct는 Swift가 자동으로 이니셜라이저를 만들어주지만, class는 자동 생성이 되지 않아 직접 작성해야 한다. @Model을 붙이면 class를 써야 하므로, 항상 init을 직접 써줘야 한다.
@Model
class Friend {
var name: String
var birthday: Date
var memo: String
// 모든 프로퍼티에 값을 넣어줘야 함
init(name: String, birthday: Date, memo: String = "") {
self.name = name
self.birthday = birthday
self.memo = memo // 기본값이 있으면 생략 가능
}
}
// 사용 예시
let f1 = Friend(name: "철수", birthday: Date()) // memo는 기본값 ""
let f2 = Friend(name: "영희", birthday: Date(), memo: "친한 친구")
3. ModelContainer
ModelContainer는 SwiftData의 데이터 저장소(창고) 역할을 한다. 내부적으로 SQLite 데이터베이스 파일을 기기의 SSD에 생성하고 관리한다. 앱을 종료하거나 기기를 꺼도 데이터가 유지되는 이유가 바로 이 때문이다.
보통 앱 최상단에 하나만 만들어서 전체 앱이 공유하도록 구성한다. 여러 모델 타입을 배열로 넘겨 한 컨테이너에서 함께 관리할 수 있다.
@main
struct BirthdaysApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// Friend 단일 모델 등록
.modelContainer(for: Friend.self)
// 여러 모델 동시 등록
// .modelContainer(for: [Friend.self, Post.self, Tag.self])
}
}
컨테이너가 뷰 계층에 여러 개 있을 경우, 각 뷰는 자신과 가장 가까운 상위 컨테이너에 연결된다.
App
├── ViewA ← .modelContainer(A 창고)
│ └── SubViewA ← A 창고 사용
└── ViewB ← .modelContainer(B 창고)
└── SubViewB ← B 창고 사용
4. ModelContext
ModelContext는 컨테이너와 뷰 사이에서 데이터를 실제로 조작하는 도구다. 창고(컨테이너)에 물건을 넣고 빼는 직원에 비유할 수 있다.
.modelContainer modifier가 SwiftUI environment에 modelContext를 자동으로 주입하기 때문에, 하위 뷰 어디서든 @Environment로 꺼내 쓸 수 있다.
주요 기능은 세 가지다.
insert()— 새 데이터를 컨테이너에 저장delete()— 데이터를 컨테이너에서 삭제fetch()— 조건에 맞는 데이터를 직접 조회 (보통은 @Query를 사용)
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var name = ""
@State private var birthday = Date()
func saveFriend() {
let newFriend = Friend(name: name, birthday: birthday)
modelContext.insert(newFriend) // 컨테이너에 저장
}
func deleteFriend(_ friend: Friend) {
modelContext.delete(friend) // 컨테이너에서 삭제
}
}
5. @Query
@Query는 컨테이너에서 특정 타입의 데이터를 자동으로 가져오고, 변화가 생기면 뷰를 자동으로 업데이트해주는 프로퍼티 래퍼다. @State와 비슷하게 뷰를 갱신해주지만, 데이터 출처가 메모리가 아닌 SwiftData 컨테이너라는 점이 다르다.
타입을 기준으로 데이터를 구분하기 때문에, 컨테이너에 여러 모델이 있어도 자신이 선언된 타입의 데이터만 가져온다.
struct ContentView: View {
// 기본 사용 — Friend 전체를 가져옴
@Query var friends: [Friend]
// 생일 기준 오름차순 정렬
@Query(sort: \Friend.birthday) var friends: [Friend]
// 생일 기준 내림차순 정렬
@Query(sort: \Friend.birthday, order: .reverse) var friends: [Friend]
// 여러 타입을 동시에 사용해도 타입별로 자동 구분
@Query var friends: [Friend]
@Query var posts: [Post]
}
modelContext.insert()로 새 데이터가 들어오면, @Query가 즉시 감지해서 friends 배열을 갱신하고 뷰를 다시 그린다. 개발자가 직접 배열에 append()할 필요가 없다.
6. DatePicker 설정
DatePicker는 날짜와 시간을 선택하는 UI 컴포넌트다. in: 파라미터로 선택 가능한 범위를 제한하고, displayedComponents로 어떤 컨트롤을 화면에 표시할지 지정한다.
in: 파라미터에는 Swift의 **닫힌 범위 연산자(...)**를 사용한다. Date.distantPast...Date.now는 "아주 먼 과거부터 현재까지"를 의미하며, 이 범위 밖인 미래 날짜는 선택할 수 없게 된다.
// 미래 날짜 선택 불가 + 날짜만 표시
DatePicker(
"생일",
selection: $birthday,
in: Date.distantPast...Date.now,
displayedComponents: .date
)
// 오늘 이후만 선택 가능 (예약 앱 등에서 활용)
DatePicker(
"예약일",
selection: $reservationDate,
in: Date.now...,
displayedComponents: [.date, .hourAndMinute]
)
| 옵션 | 표시 내용 |
|---|---|
.date |
연, 월, 일 |
.hourAndMinute |
시, 분 |
.hourMinuteAndSecond |
시, 분, 초 |
[.date, .hourAndMinute] |
날짜 + 시/분 조합 |
[.date, .hourMinuteAndSecond] |
날짜 + 시/분/초 조합 |
7. Calendar
Date는 특정 시점을 나타내는 절대값이기 때문에, 사람이 이해하는 "몇 월 며칠"로 변환하려면 Calendar가 필요하다. Calendar.current는 기기에 설정된 달력(양력, 음력 등)을 자동으로 사용하므로, 전 세계 사용자에게 올바르게 동작한다.
@Model
class Friend {
var name: String
var birthday: Date
// 오늘 생일인지 확인하는 계산 프로퍼티
var isBirthdayToday: Bool {
let calendar = Calendar.current
let todayComponents = calendar.dateComponents([.month, .day], from: Date.now)
let birthComponents = calendar.dateComponents([.month, .day], from: birthday)
return todayComponents.month == birthComponents.month
&& todayComponents.day == birthComponents.day
}
// 몇 살인지 계산
var age: Int {
Calendar.current.dateComponents([.year], from: birthday, to: Date.now).year ?? 0
}
}
8. inMemory 컨테이너
inMemory: true로 설정하면 데이터를 파일이 아닌 메모리에만 저장한다. 앱이 종료되거나 프리뷰가 새로 고침되면 데이터가 사라진다.
프리뷰에서 사용하면 매번 깨끗한 초기 상태로 시작할 수 있어, 테스트와 UI 확인이 편리해진다. 반면 실제 앱(시뮬레이터 포함)은 기본적으로 파일 저장소를 사용하므로, 앱을 재실행해도 데이터가 유지된다.
// 프리뷰에서는 inMemory: true
#Preview {
ContentView()
.modelContainer(for: Friend.self, inMemory: true)
}
// 실제 앱에서는 기본값(파일 저장소) 사용
.modelContainer(for: Friend.self)
9. .bold() 조건부 적용
.bold()는 인수 없이 쓰면 항상 굵게 표시되고, Bool 값을 넣으면 조건부로 굵게 표시할 수 있다.
// 항상 굵게
Text(friend.name)
.bold()
// 오늘 생일인 친구만 굵게
Text(friend.name)
.bold(friend.isBirthdayToday)
// 응용: 생일인 친구에게 케이크 아이콘도 추가
HStack {
Text(friend.name)
.bold(friend.isBirthdayToday)
if friend.isBirthdayToday {
Image(systemName: "birthday.cake")
.foregroundStyle(.pink)
}
}
10. 스와이프 삭제 (.onDelete)
.onDelete는 SwiftUI List 안의 ForEach에 붙여서 왼쪽 스와이프 삭제 기능을 구현하는 modifier다. HStack 등 다른 뷰에는 직접 붙일 수 없고, 반드시 ForEach 뒤에 붙여야 한다.
삭제 시 전달되는 IndexSet은 사용자가 삭제하려는 항목의 인덱스를 담고 있다. SwiftData를 사용할 때는 modelContext.delete()로 컨테이너에서 삭제하면, @Query가 자동으로 감지해서 리스트도 함께 갱신된다.
List {
ForEach(friends) { friend in
HStack {
VStack(alignment: .leading) {
Text(friend.name)
.bold(friend.isBirthdayToday)
Text(friend.birthday, style: .date)
.foregroundStyle(.secondary)
}
Spacer()
if friend.isBirthdayToday {
Image(systemName: "birthday.cake")
}
}
}
// .onDelete는 ForEach 바로 뒤에
.onDelete { indexSet in
for index in indexSet {
modelContext.delete(friends[index])
}
}
}
2026년 5월 27일 AM 3:01
로그인 필요