app

김태헌

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

댓글 닫기
댓글이 없습니다.
로그인 필요