스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftData 자세히 살펴보기
앱에서 SwiftData의 강력한 기능을 활용하는 방법을 알아보세요. ModelContext와 ModelContainer가 함께 작동하여 앱의 데이터를 지속하는 방식을 살펴보겠습니다. 또한 수동으로 변경 사항을 추적하고 적용하는 방법과 FetchDescriptor, SortDescriptor, enumerate로 SwiftData를 규모에 맞게 사용하는 방법을 알려 드립니다. 이 세션을 최대한 활용하려면 WWDC23의 'SwiftData 알아보기'와 'SwiftData로 스키마 모델링하기' 세션을 먼저 시청하시길 권합니다.
챕터
- 0:00 - Intro
- 3:42 - Configuring persistence
- 7:21 - Track and persist changes
- 11:20 - Modeling at scale
- 14:54 - Wrap-up
리소스
관련 비디오
WWDC23
-
다운로드
♪ ♪
안녕하세요, 닉 질렛입니다 Apple의 SwiftData 팀 엔지니어죠 이번 세션에서는 SwiftData로 빌드한 응용 프로그램이 어떻게 발전하여 강력한 프레임워크를 활용하는지 자세히 알아보겠습니다 우선 응용 프로그램에서 지속성을 구성하는 방법을 살펴보고 ModelContext를 사용해 변경 사항을 추적하고 지속하는 법을 알려 드릴게요 마지막으로 코드에서 객체를 다룰 때 SwiftData를 최대한 활용하는 법을 알려 드리죠 이 세션은 'SwiftData 알아보기'와 'SwiftData로 스키마 모델링하기'에서 소개된 개념과 API를 토대로 진행됩니다 이 세션을 진행하기 전에 해당 세션을 복습하시길 강력히 권합니다 이번 영상에서는 SampleTrips라는 새로운 샘플 응용 프로그램을 참조합니다 SwiftData로 응용 프로그램을 구축하는 게 얼마나 쉬운지 보여 드리려고 올해 만든 응용 프로그램이죠 SampleTrips를 사용하면 여행할 시기와 장소를 쉽게 정리할 수 있는데요 SwiftData를 통해 실행 취소나 응용 프로그램 전환 시 자동 저장 등 표준 플랫폼 기능을 쉽게 구현할 수 있죠 SwiftData는 Swift를 쓰는 응용 프로그램의 데이터를 지속하는 새로운 방법입니다 Classes나 Structs 등 코드에서 이미 사용 중인 유형과 함께 작동하도록 디자인됐죠 이 개념의 핵심인 모델은 새 매크로 @Model로 설명됩니다 지속해야 하는 유형을 SwiftData에 알려 주죠 다음은 SampleTrips 응용 프로그램의 Trip 클래스입니다 여행 정보를 캡처하는 몇 가지 프로퍼티와 SampleTrips에서 쓰이는 다른 객체에 대한 참조가 있죠 SwiftData는 지금 보여 드린 코드처럼 지속성 없이 작성하는 코드와 지속성이 필요한 코드 사이의 차이를 최소한으로 제공합니다 약간의 수정으로 SwiftData에 해당 Trip이 지속하고 싶은 모델임을 알렸고 BucketListItem 및 LivingAccommodations와의 관계가 어떻게 동작해야 하는지 서술했죠 SwiftData는 작성된 코드에서 여러분이 원하는 구조를 자동으로 추론할 수도 있습니다 또한 SwiftData는 강력한 사용자화 기능을 제공하여 원하는 데이터 저장 방식을 정확히 서술할 수 있게 해 주죠 모델의 강력한 기능을 자세히 알아보시려면 'SwiftData로 스키마 모델링하기' 세션을 참조하세요 이러한 주석을 통해 Trip 클래스는 SwiftData에서 두 가지 중요한 역할을 맡습니다 첫 번째는 응용 프로그램의 객체 그래프인 스키마를 서술하는 것이고 두 번째는 코드를 작성할 수 있는 인터페이스가 되는 것이죠 이러한 이중성 즉 두 역할을 수행하는 능력이 @Model 매크로로 주석이 달린 클래스를 SwiftData를 사용하는 응용 프로그램의 접촉 중심점으로 만듭니다 정렬된 API 개념이 이러한 역할을 지원하죠
스키마는 ModelContainer라는 클래스에 적용되어 데이터의 지속 방식을 서술합니다 ModelContainer는 스키마를 소모하여 모델 클래스의 인스턴스를 보유하는 데이터베이스를 만들죠 코드에서 모델 클래스의 인스턴스를 다룰 때는 해당 인스턴스가 ModelContext에 연결됩니다 ModelContext는 메모리에서 인스턴스 상태를 추적 및 관리하죠 이러한 이중성은 SwiftData의 핵심입니다 이번 섹션에서는 지속성의 구조를 설명하는 모델의 첫 번째 역할을 살펴보고 이것이 ModelContainer와 함께 작동하는 방식을 알아볼게요 ModelContainer는 데이터가 기기에서 저장되고 지속되는 방식을 서술합니다 ModelContainer를 스키마와 스키마의 지속성을 잇는 다리로 생각할 수 있습니다 객체가 메모리와 디스크 중 어디에 저장되는지 등의 저장 방식에 대한 서술이 버전 관리와 마이그레이션 그래프 분리 같은 스토리지의 운영 및 개발 시맨틱과 만나는 곳이죠 스키마로 컨테이너를 인스턴스화하는 건 쉽습니다 작업하려는 유형만 제공하면 SwiftData가 나머지 스키마를 처리해 주죠 예를 들어 Trip 클래스가 다른 모델 유형과 관련하므로 ModelContainer가 이 스키마를 추론합니다 유형 하나만 전달해도요 ModelContainer에는 코드와 함께 성장하는 강력한 이니셜라이저가 많으며 ModelConfiguration이라는 클래스를 사용해 구성을 점점 복잡하게 할 수 있습니다 ModelConfiguration은 스키마의 지속성을 서술합니다 데이터의 저장 위치를 제어하죠 일시 데이터는 메모리에 영구 데이터는 디스크에 저장하는 식입니다 ModelConfiguration은 여러분이 선택한 특정 파일 URL을 쓰거나 그룹 컨테이너 권한 등 응용 프로그램의 권한을 사용하여 자동으로 URL을 생성합니다 또한 구성은 영구 파일이 읽기 전용 모드로 로드되어야 함을 서술하여 민감한 데이터나 템플릿 데이터에 대한 작성을 방지할 수도 있죠 마지막으로 두 개 이상의 CloudKit 컨테이너를 쓰는 응용 프로그램은 이를 스키마 ModelConfiguration의 일부로 지정할 수 있습니다 새로운 Person 클래스와 Address 클래스를 사용해 SampleTrips에 연락처 정보를 추가한다고 해 보죠 우선 사용할 유형이 모두 포함된 전체 스키마를 선언합니다 다음으로 SampleTrips 데이터에 구성을 선언합니다 Trip과 BucketListItem LivingAccommodations 모델을 포함하죠 이는 해당 객체 그래프의 데이터 저장에 사용할 파일의 URL을 선언합니다 SampleTrips 데이터를 CloudKit에 동기화할 때 사용할 CloudKit 컨테이너의 컨테이너 식별자도요 이제 Person 및 Address가 포함된 새 스키마의 모델을 자체 구성에 선언합니다 CloudKit 컨테이너 식별자와 고유한 파일 URL과 함께요 그러면 Trips 그래프에서 데이터를 분리할 수 있죠 마지막으로 스키마와 구성이 결합하여 ModelContainer를 형성합니다
ModelConfiguration의 기능으로 응용 프로그램의 지속성 요구 사항을 간단히 서술할 수 있습니다 아무리 복잡한 요구 사항이라도요 수동으로 컨테이너를 인스턴스화하는 것뿐만 아니라 SwiftUI 응용 프로그램은 새 ModelContainer 수정자로 원하는 컨테이너를 생성할 수 있습니다 ModelContainer 수정자는 응용 프로그램의 어떤 뷰나 씬에도 추가 가능하며 간단한 것부터 강력한 것까지 다양한 ModelContainer를 지원합니다 지금까지 ModelContainer로 스키마를 지속성과 통합하는 법을 알아봤습니다 이는 응용 프로그램과 함께 발전하며 응용 프로그램의 기능과 객체 그래프가 강력해질수록 향상하죠 또한 ModelConfiguration을 사용하여 강력한 지속성 기능을 활성화하는 방법도 살펴봤습니다 'SwiftData 알아보기'에서 배웠듯 모델과 ModelContext는 사용자 인터페이스를 작성하거나 모델 객체를 운용할 때 가장 자주 쓰이는 개념입니다 이번 섹션에서는 ModelContext가 어떻게 변경 사항을 추적하고 ModelContainer로 편집 내용을 지속하는지 알아보죠 뷰나 씬 코드에서 modelContainer 수정자를 사용하면 특정한 방식으로 응용 프로그램 환경을 준비합니다 수정자가 환경의 새로운 modelContext 키를 컨테이너의 mainContext에 바인딩하죠 메인 콘텍스트는 특수한 MainActor 정렬 모델 콘텍스트로 씬이나 뷰에서 ModelObjects와 함께 작동합니다 환경의 모델 콘텍스트를 사용하면 뷰 코드가 쿼리에 쓰인 콘텍스트에 쉽게 액세스할 수 있습니다 그래서 삭제 같은 작업을 수행할 수 있죠 따라서 모델 콘텍스트는 사용과 액세스가 쉽지만 실제 역할은 뭘까요? ModelContext는 응용 프로그램이 관리하는 데이터에 대한 뷰로 생각할 수 있습니다 작업할 데이터는 사용 중인 모델 콘텍스트로 페칭됩니다 SampleTrips의 경우 예정된 여행의 뷰가 목록에 데이터를 로드할 때 각 여행 객체를 메인 콘텍스트로 가져오죠 여행을 수정하면 해당 변경 사항이 스냅숏으로 모델 콘텍스트에 기록됩니다 새 여행을 추가하거나 기존 여행을 삭제하는 등 다른 변경 사항이 발생하면 콘텍스트가 이러한 변경의 상태를 추적하고 유지하다가 'context.save()'를 호출하면 멈춥니다 삭제된 여행이 목록에서 더는 보이지 않더라도 저장을 호출하여 해당 삭제를 지속할 때까지는 ModelContext에 남아 있다는 뜻이죠
저장이 호출되면 콘텍스트가 변경 사항을 ModelContainer에 지속시키고 상태를 지웁니다 목록에 표시하는 등 콘텍스트에서 객체를 계속 참조한다면 해당 객체는 사용이 끝날 때까지 콘텍스트에 남게 됩니다 그 시점에 객체가 해제되고 콘텍스트가 비워지죠 ModelContext는 바인딩된 ModelContainer와 함께 작동합니다 뷰에서 가져온 객체를 추적하고 저장이 실행될 때 변경 사항을 전파하죠 또한 ModelContext는 롤백과 재설정 같은 기능도 지원해 필요시 캐싱된 상태를 지울 수 있습니다 그래서 실행 취소 및 자동 저장 등의 플랫폼 기능을 지원하기에 이상적이죠 SwiftUI 응용 프로그램에서 ModelContainer 수정자는 isUndoEnabled 인수를 지닙니다 윈도우의 undoManager를 컨테이너의 mainContext에 바인딩하는 인수죠 다시 말해 메인 콘텍스트에 변경 사항이 생기면 세 손가락으로 밀기나 흔들기 등의 시스템 제스처로 추가 코드 없이 변경을 실행 취소 혹은 실행 복귀 할 수 있습니다 모델 객체에 변경이 발생하면 ModelContext는 자동으로 실행 취소 및 실행 복귀 동작을 등록합니다 ModelContainer 수정자가 쓰는 환경의 undoManager는 윈도우나 윈도우 그룹의 일부로 시스템에서 제공됩니다 그래서 세 손가락으로 밀기나 흔들기 같은 시스템 제스처가 응용 프로그램에서 자동으로 작동하죠 ModelContext가 지원하는 또 다른 표준 시스템 기능은 바로 자동 저장입니다 자동 저장이 활성화되면 모델 콘텍스트는 시스템 이벤트에 저장됩니다 포그라운드나 백그라운드로 응용 프로그램이 전환되는 등의 이벤트죠 메인 콘텍스트도 응용 프로그램이 쓰이는 동안 주기적으로 저장됩니다 자동 저장은 응용 프로그램에서 기본적으로 활성화되며 필요시 비활성화할 수 있습니다 ModelContainer 수정자의 isAutosaveEnabled 인수로요 수동으로 만든 모델 콘텍스트에선 자동 저장이 비활성화됩니다 'SwiftData 알아보기'에서 응용 프로그램에서 ModelContext로 작업하는 방법과 이것이 SwiftUI와 얼마나 잘 페어링되는지 배웠죠 하지만 응용 프로그램이 모델 객체를 사용하는 곳은 사용자 인터페이스뿐만이 아닙니다 이번 섹션에서는 SwiftData로 쉽고 안전하게 강력하고 확장 가능한 코드를 작성하는 법을 보여 드리죠 백그라운드 큐에서 데이터를 다루는 작업과 원격 서버나 다른 영구 메커니즘과의 동기화 배치 처리는 모두 모델 객체를 동반하죠 주로 집합이나 그래프 형태로요 이 중 많은 작업이 ModelContext의 페치 메서드로 작업할 객체 집합을 가져옵니다 이 예제에서는 Trip 모델의 FetchDescriptor로 여행 어레이가 Trip 객체 컬렉션임을 Swift에 알립니다 캐스팅이나 복잡한 결과 투플을 걱정할 필요가 없죠
FetchDescriptor로 손쉽게 복잡한 쿼리를 만들 수 있습니다 새 Predicate 매크로를 쓰면 되죠 예를 들어 특정 호텔에서 숙박하는 여행을 찾거나 활동을 예약해야 하는 여행을 찾을 수 있습니다 SwiftData에서는 하위 쿼리와 조인을 지원하는 복잡한 쿼리도 모두 순수한 Swift로 작성할 수 있습니다 Predicate는 여러분이 만든 모델을 쓰고 SwiftData는 그 모델에서 생성된 스키마를 사용하여 이러한 술부를 데이터베이스 쿼리로 변환합니다 FetchDescriptor는 새로운 Foundation Predicate 매크로의 기능을 스키마와 결합하여 컴파일러 인증 쿼리를 Apple 플랫폼에서 지속시켜 줍니다 FetchDescriptor와 SortDescriptor 등 관련 클래스는 제네릭으로 결과 유형을 형성하고 사용 가능한 모델 프로퍼티를 컴파일러에 알려 주죠 여러 가지 조정 옵션을 알고 계실 텐데요 offset과 limit faulting 및 prefetching 매개변수도 있죠
이 모든 기능은 ModelContext의 enumerate 함수에서 결합됩니다 이는 암시적으로 배치 순회와 열거의 복잡한 패턴을 효율적으로 만들어 주죠 하나의 호출 사이트에 플랫폼 모범 사례를 캡슐화해서요 enumerate는 FetchDescriptor가 아무리 복잡해도 훌륭히 작동합니다 간단한 것부터 강력한 것까지 모두 지원하죠 enumerate는 배칭이나 변형 가드 등의 플랫폼 모범 사례를 자동으로 구현합니다 이는 사용 예에 맞게 사용자화할 수 있죠 예를 들어 enumerate 배치의 기본 크기는 객체 5천 개인데요 순회 중 I/O 작업을 줄이기 위해 1만으로 변경할 수 있죠 메모리 증가를 감안하고요 이미지나 동영상 기타 고용량 데이터를 포함하는 무거운 객체 그래프의 경우 더 작은 배치 크기를 사용할 수 있습니다 배치 크기를 줄이면 메모리 증가는 감소하지만 열거 중 I/O가 증가합니다 또한 enumerate는 기본적으로 변형 가드를 포함합니다 규모가 큰 순회에서 발생하는 성능 문제는 대부분 열거 중 콘텍스트에 갇힌 변형 때문입니다 allowEscapingMutations로 의도된 결과임을 enumerate에 알릴 수 있죠 설정하지 않은 경우 열거를 수행하는 ModelContext가 더티로 발견되면 enumerate가 예외를 발생시켜 이미 순회한 객체의 해제를 막습니다 이번 세션에서는 스키마와 ModelConfiguration으로 효과적인 지속성을 구성하는 법을 알아봤습니다 또한 ModelContainer와 ModelContext로 실행 취소와 실행 복귀 등 표준 시스템 기능을 쉽게 적용하는 방법도 배웠죠 이제 FetchDescriptor와 술부 enumerate를 통해 이전보다 안전하고 우수한 코드를 SwiftData에서 작성할 수 있습니다 앞으로 여러분이 수개월에서 수년 동안 이 새로운 프레임워크의 가능성을 얼마나 끌어올릴지 기대됩니다 시청해 주셔서 감사드리며 즐거운 코딩하세요
-
-
1:45 - Trip model with cascading relationships
@Model final class Trip { var destination: String? var end_date: Date? var name: String? var start_date: Date? @Relationship(.cascade) var bucketListItem: [BucketListItem] = [BucketListItem]() @Relationship(.cascade) var livingAccommodation: LivingAccommodation? }
-
4:21 - Initializing a ModelContainer
// ModelContainer initialized with just Trip let container = try ModelContainer(for: Trip.self) // SwiftData infers related model classes as well let container = try ModelContainer( for: [ Trip.self, BucketListItem.self, LivingAccommodation.self ] )
-
5:41 - Using ModelConfiguration to customize ModelContainer
let fullSchema = Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self, Person.self, Address.self ]) let trips = ModelConfiguration( schema: Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self ]), url: URL(filePath: "/path/to/trip.store"), cloudKitContainerIdentifier: "com.example.trips" ) let people = ModelConfiguration( schema: Schema([Person.self, Address.self]), url: URL(filePath: "/path/to/people.store"), cloudKitContainerIdentifier: "com.example.people" ) let container = try ModelContainer(for: fullSchema, trips, people)
-
6:49 - Creating ModelContainer in SwiftUI
@main struct TripsApp: App { let fullSchema = Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self, Person.self, Address.self ]) let trips = ModelConfiguration( schema: Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self ]), url: URL(filePath: "/path/to/trip.store"), cloudKitContainerIdentifier: "com.example.trips" ) let people = ModelConfiguration( schema: Schema([ Person.self, Address.self ]), url: URL(filePath: "/path/to/people.store"), cloudKitContainerIdentifier: "com.example.people" ) let container = try ModelContainer(for: fullSchema, trips, people) var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
7:40 - Using the modelContainer modifier
@main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } }
-
7:50 - Referencing a ModelContext in SwiftUI views
struct ContentView: View { @Query var trips: [Trip] @Environment(\.modelContext) var modelContext var body: some View { NavigationStack (path: $path) { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) .swipeActions(edge: .trailing) { Button(role: .destructive) { modelContext.delete(trip) } label: { Label("Delete", systemImage: "trash") } } } .onDelete(perform: deleteTrips(at:)) } } } }
-
9:57 - Enabling undo on a ModelContainer
@main struct TripsApp: App { @Environment(\.undoManager) var undoManager var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, isUndoEnabled: true) } }
-
11:05 - Enabling autosave on a ModelContainer
@main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, isAutosaveEnabled: false) } }
-
11:54 - Fetching objects with FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) var trips = try context.fetch(FetchDescriptor<Trip>())
-
12:14 - Fetching objects with #Predicate and FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) let hotelNames = ["First", "Second", "Third"] var predicate = #Predicate<Trip> { trip in trip.livingAccommodations.filter { hotelNames.contains($0.placeName) }.count > 0 } var descriptor = FetchDescriptor(predicate: predicate) var trips = try context.fetch(descriptor)
-
12:27 - Fetching objects with #Predicate and FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) predicate = #Predicate<Trip> { trip in trip.livingAccommodations.filter { $0.hasReservation == false }.count > 0 } descriptor = FetchDescriptor(predicate: predicate) var trips = try context.fetch(descriptor)
-
13:18 - Enumerating objects with FetchDescriptor
context.enumerate(FetchDescriptor<Trip>()) { trip in // Operate on trip }
-
13:36 - Enumerating with FetchDescriptor and SortDescriptor
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate(descriptor) { trip in // Remind me to make reservations for trip }
-
14:01 - Fine tuning enumerate with batchSize
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate( descriptor, batchSize: 10000 ) { trip in // Remind me to make reservations for trip }
-
14:28 - Fine tuning enumerate with batchSize and allowEscapingMutations
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate( descriptor, batchSize: 500, allowEscapingMutations: true ) { trip in // Remind me to make reservations for trip }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.