• 샘플앱: https://developer.apple.com/documentation/SwiftUI/Building-a-document-based-app-using-SwiftData

  • 모델링

    • 기존 ObservableObject 기반 모델

      final class Card: ObservableObject [
      	@Published var front: String
      	@Published var back: String
      	var creationDate: Date
      
      	init(front: String, back: String, creationDate: Date = .now) { ... }
      }
      
      extension Card: Identifiable { }
      
      extension Card: Hashable {
      	// ...
      }
      
    • SwiftData에서 쓰기 위한 모델로 변경

      • @Model은 @Observable 매크로를 포함한다. 따라서 ObservableObject와 Published 프로퍼티 래퍼를 없앨 수 있다.

        @Model
        final class Card {
            var front: String
            var back: String
            var creationDate: Date
        
            init(front: String, back: String, creationDate: Date = .now) {
                self.front = front
                self.back = back
                self.creationDate = creationDate
            }
        }
        
    • 뷰에서는 텍스트 입력을 위해서 binding이 필요하기 때문에 Bindable로 해준다.

      @Bindable var card: Card
      
  • UI에 보여주기 위한 모델 쿼리

    • 뷰에서 SwiftData를 import하고 @State를 @Query로 바꿔준다.

      @Query private var cards: [Card]
      
    • @Query

      • 프로퍼티 래퍼
      • SwiftData에 있는 데이터에 대한 view를 제공
      • 모델에 변경이 있을 때마다 뷰 업데이트가 트리거된다.
      • 한 뷰가 여러개의 쿼리를 가질 수도 있다.
      • 정렬 및 정렬 방향, 필터, 애니매이션 등을 지정할 수 있는 옵션도 제공한다.
      • ModelContext를 데이터 소스로 사용한다.
        • ModelContainer를 통해서 가져온다.
    • ModelContainer 설정하기

      • 최소 1개 이상의 모델 컨테이너를 설정해야 한다.
      • 전체 스택을 자동으로 만들어준다.
      • 뷰가 가질 수 있는 컨테이너는 하나지만, 앱은 여러개의 컨테이너를 가질 수 있다.
      • 모델 컨테이너를 설정하면 해당 뷰계층에서 같은 컨테이너를 공유하고, 하위에서 modelContainer를 오버라이드할 수 있다.
        • 서로 다른 모델 컨테이너는 서로에게 영향을 주지 않는다.
        • 서브트리에서는 컨테이너를 만들 때 명시한 타입의 모델만 다룰 수 있다.
      WindowGroup {
          ContentView()
      }
      .modelContainer(for: Card.self)
      
    • preview용 샘플 컨테이너 만들기

      import SwiftUI
      import SwiftData
      
      @MainActor
      let previewContainer: ModelContainer = {
      	do {
      		let container = try ModelContainer(
      			for: Card.self, ModelConfiguration(inMemory: true)
      		)
      
      		for card in SampleDeck.contents {
      			contents.mainContext.insert(object: card)
      		}
      
      		return container
      	} catch {
      		fatalError("failed to create container")
      	}
      }()
      
       // preview
      
      #Preview {
          ContentView()
              .frame(minWidth: 500, minHeight: 500)
              .modelContainer(previewContainer)
      }
      
  • 모델 생성 및 갱신

    • modelContext에 접근해야 한다.

      • container처럼 뷰는 하나의 context만 가진다.
      • modelContainer를 설정하면 해당 environment값이 자동으로 설정된다.
      @Environment(\\.modelContext) private var modelContext
      
    • 새 모델 넣기

      • UI에 관련된 이벤트나 사용자 입력에 맞춰서 자동적으로 저장된다.
        • 즉시 저장이 필요한 경우(저장소를 공유하거나 전송해야 한다던지)에만 명시적으로 save를 호출한다.
      let newCard = Card(front: "Sample Front", back: "Sample Back")
      modelContext.insert(object: newCard)
      
  • [Bonus] Document-based app

    • 사용자가 직접 다양한 타입의 document를 만들고, 열고, 보고, 편집할 수 있게 해주는 애플리케이션

      • 모든 document는 파일이여서 저장, 복사, 공유가 가능하다.
    • SwiftData-backed Document-based app

      • DocumentGroup 생성자에 원하는 모델 타입을 명시해서 만든다.
      • iOS, iPadOS, MacOS에서만 지원하는 뷰기 때문에 분기처리한다.
      @main
      struct SwiftDataFlashCardSample: App {
          var body: some Scene {
              #if os(iOS) || os(macOS)
              DocumentGroup(editing: Card.self, contentType: <#UTType#>) {
                  <#code#>
              }
              #else
              WindowGroup {
                  ContentView()
                      .modelContainer(for: Card.self)
              }
              #endif
          }
      }
      
    • contentType

      • document는 바이너리 형태일 수도 있고, 패키지 형태일 수도 있다.

        • SwiftData로 저장되는 document는 패키지다.
        • 그래서 contentType은 com.apple.package를 상속받아야 한다.
      • 두 경우 모두 고정된 구조로 이루어져야만 다른 곳에서도 읽을 수 있다.

      • 사용자가 커스텀 데이터 구조를 읽기 위해서는 OS가 어떤 앱에서 이를 읽을 수 있을지 알아야 한다. 이를 알 수 있게 해주는 것이 ContentType이다.

      • 그래서 각 SwiftData 모델은 개별적인 ContentType을 가져야 한다.

        • plist에도 관련 정보가 들어가야 OS가 이를 볼 수 있다.
        import UniformTypeIdentifiers
        
        extension UTType {
        	static var flashCards = UTType(exportedAs: "com.example.flashCards")
        }
        
    • contentType까지 명시하기

      • 각 Document마다 자동으로 Container를 설정하기 때문에 따로 해줄 필요는 없다.
      @main
      struct SwiftDataFlashCardSample: App {
          var body: some Scene {
              #if os(iOS) || os(macOS)
              DocumentGroup(editing: Card.self, contentType: .flashCards) {
                  ContentView()
              }
              #else
              WindowGroup {
                  ContentView()
                      .modelContainer(for: Card.self)
              }
              #endif
          }
      }