• SwiftData 복습

    • 앱의 모델 레이어를 쉽게 만들고 앱 실행 간에 영속적으로 저장하게 해주는 프레임워크

    • 영속성 뿐 아니라 모델링, 스키마 마이그레이션, 그래프 관리, CloudKit을 통한 동기화 등의 다양한 기능을 제공한다.

      스크린샷 2024-06-17 오후 11.41.17.png

    • 기본 사용법

      • 모델에 @Model 매크로를 붙인다.

        import Foundation
        import SwiftData
        
        @Model
        class Trip {
            var name: String
            var destination: String
            var startDate: Date
            var endDate: Date
            
            var bucketList: [BucketListItem] = [BucketListItem]()
            var livingAccommodation: LivingAccommodation?
        }
        
        @Model
        class BucketListItem {...}
        
        @Model
        class LivingAccommodation {...}
        
      • 모델 컨테이너를 뷰에서 지정

        // Trip App using modelContainer Scene modifier
        import SwiftUI
        import SwiftData
        
        @main
        struct TripsApp: App {
            var body: some Scene {
                WindowGroup {
                    ContentView
                }
                .modelContainer(for: Trip.self)
            }
        }
        
      • 데이터를 컨테이너에서 쿼리

        import SwiftUI
        import SwiftData
        
        struct ContentView: View {
            @Query
            var trips: [Trip]
            var body: some View {
                NavigationSplitView {
                    List(selection: $selection) {
                        ForEach(trips) { trip in
                            TripListItem(trip: trip)
                        }
                    }
                }
            }
        }
        
  • Cusomize the Schema

    • 기존에 스키마 커스텀을 위해서 사용하던 매크로들이 있었다.

      • @Attribute
      • @Relationship
      • @Transient
    • 올해 추가된 매크로

      • #Unique
        • 같은 데이터를 나타내는 KeyPath를 정의.
        • 해당 keyPath로 나타나는 데이터는 컨테이너 내에서 유일해야한다.
        • 충돌이 일어나면 upsert를 수행한다.
      import SwiftData
      
      @Model 
      class Trip {
          #Unique<Trip>([\\.name, \\.startDate, \\.endDate])
          
          var name: String
          var destination: String
          var startDate: Date
          var endDate: Date
          
          var bucketList: [BucketListItem] = [BucketListItem]()
          var livingAccommodation: LivingAccommodation?
      }
      
    • preserveValueOnDeletion

      • upsert된 값을 보존해서 History API를 통해서 가져올 수 있게 해준다.
      • 모델이 삭제되면 히스토리에 tombstone 값으로 보존되어서 나중에 이를 받아서 처리할 수 있게 해준다.
      • 커스텀 데이터 저장소와도 동작한다.
      • 자세한 것은 Track model changes with SwiftData history참조
  • Tailor a container

    • modelContainer modifier를 그냥 쓰면 SwiftUI가 컨테이너를 자동으로 설정해준다.

      @main
      struct TripsApp: App {   
          var body: some Scene {
              WindowGroup {
                  ContentView()
              }
              .modelContainer(for: Trip.self)
         }
      }
      
    • 몇가지 커스텀도 가능하다.

      // Customize a model container in the app
      import SwiftUI
      import SwiftData
      
      @main
      struct TripsApp: App {   
          var body: some Scene {
              WindowGroup {
                  ContentView()
              }
              .modelContainer(for: Trip.self,
                              inMemory: true,
                              isAutosaveEnabled: true,
                              isUndoEnabled: true)
         }
      }
      
    • 이 상으로 커스텀하기 위해서는 직접 Container 인스턴스를 만들어야 한다.

      import SwiftUI
      import SwiftData
      
      @main
      struct TripsApp: App {
          var container: ModelContainer = {
              do {
                  let configuration = ModelConfiguration(schema: Schema([Trip.self]), url: fileURL)
                  return try ModelContainer(for: Trip.self, configurations: configuration)
              }
              catch { ... }
          }()
          
         var body: some Scene {
              WindowGroup {
                  ContentView()
              }
              .modelContainer(container)
         }
      }
      
    • 아예 저장소를 커스텀하게 만들어 쓸 수도 있다.

      • 기존 SwiftData API를 사용한다.
      • 기능을 점진적으로 적용하는 기능을 제공한다.
      • 자세한 건 Create a custom data store with SwiftData 세션 참조
      import SwiftUI
      import SwiftData
      
      @main
      struct TripsApp: App {
          var container: ModelContainer = {
              do {
                  let configuration = JSONStoreConfiguration(schema: Schema([Trip.self]), url: jsonFileURL)
                  return try ModelContainer(for: Trip.self, configurations: configuration)
              }
              catch { ... }
          }()
          
         var body: some Scene {
              WindowGroup {
                  ContentView()
              }
              .modelContainer(container)
         }
      }
      
    • ex.프리뷰를 위한 인메모리 저장소

      struct SampleData: PreviewModifier {
          static func makeSharedContext() throws -> ModelContainer {
              let config = ModelConfiguration(isStoredInMemoryOnly: true)
              let container = try ModelContainer(for: Trip.self, configurations: config)
              Trip.makeSampleTrips(in: container)
              return container
          }
          
          func body(content: Content, context: ModelContainer) -> some View {
              content.modelContainer(context)
          }
      }
      
      extension PreviewTrait where T == Preview.ViewTraits {
          @MainActor static var sampleData: Self = .modifier(SampleData())
      }
      
      import SwiftUI
      import SwiftData
      
      struct ContentView: View {
          @Query
          var trips: [Trip]
      
          var body: some View {
              ...
          }
      }
      
      #Preview(traits: .sampleData) {
          ContentView()
      }
      
    • 프리뷰에서 바로 쿼리해서 데이터 꺼내기

      import SwiftUI
      import SwiftData
      
      #Preview(traits: .sampleData) {
          @Previewable @Query var trips: [Trip]
          BucketListItemView(trip: trips.first)
      }
      
  • Optimize queries

    • 쿼리는 모델의 배열을 쉽게 정렬 및 필터링하고 뷰를 동작시킬 수 있게 해준다.

      • ModelContainer의 변화에 자동적으로 반응한다.
    • #Predicate를 통해서 데이터 쿼리 중에 필터링을 평가하여 메모리에 전체 데이터를 안올리고도 동작한다.

      let predicate = #Predicate<Trip> {
          searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText)
      }
      
    • 17.4부터는 복합적인 판단식을 작성할 수 있다.

      let predicate = #Predicate<Trip> {
          searchText.isEmpty ? true :
          $0.name.localizedStandardContains(searchText) ||
          $0.destination.localizedStandardContains(searchText)
      }
      
    • iOS18부터는 Expression 매크로를 통해서 복잡한 판단식을 쉽게 작성할 수 있다.

      • true/false가 아닌 임의의 타입을 참조할 수 있게 해준다.
      • 판단식과 합쳐서 더 복잡한 쿼리를 작성할 수 있다.
      // Build a predicate to find Trips with BucketListItems that are not in the plan
      
      let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
          items.filter {
              !$0.isInPlan
          }.count
      }
      
      let today = Date.now
      let tripsWithUnplannedItems = #Predicate<Trip>{ trip
          // The current date falls within the trip
          (trip.startDate ..< trip.endDate).contains(today) &&
      
          // The trip has at least one BucketListItem
          // where 'isInPlan' is false
          unplannedItemsExpression.evaluate(trip.bucketList) > 0
      }
      
    • #Index 매크로

      • 모델의 단일 혹은 복합 인덱스를 만든다.

      • 책의 목차처럼 Index는 SwiftData가 만드는 추가 데이터를 의미하고 컨테이너에 저장된다.

      • 지정된 keyPath에 대한 쿼리를 빠르고 효율적으로 만들어준다.

        import SwiftData
        
        @Model 
        class Trip {
            #Unique<Trip>([\\.name, \\.startDate, \\.endDate
            #Index<Trip>([\\.name], [\\.startDate], [\\.endDate], [\\.name, \\.startDate, \\.endDate])
        
            var name: String
            var destination: String
            var startDate: Date
            var endDate: Date
            
            var bucketList: [BucketListItem] = [BucketListItem
            var livingAccommodation: LivingAccommodation
        }