[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/SwiftData & CoreData

[SwiftData] Meet SwiftData

inu 2023. 7. 29. 20:22
반응형

https://developer.apple.com/videos/play/wwdc2023/10187/?time=463

해당 게시글은 WWDC 2023의 Meet SwiftData 세션을 글로 정리한 것입니다.


Meet SwiftData

SwiftData는 Swift의 Macro 시스템을 적극 활용중이며 SwiftUI는 물론이고 CloudKit, Widget과도 자연스럽게 통합됨. 해당 세션에서는 다음 세가지를 설명함.

  • @Model 매크로를 통해 Swift 코드 내에서 모델을 직접 만드는 방법
  • 내 데이터를 fetch하고 modify하는 방법
  • SwiftUI와 어떻게 원활하게 작동하는지에 대한 개요

Using the model macro

@Model는 Swift Code 내에서 Model Schema를 정의할 수 있도록 도와주는 Swift의 새로운 Macro이다. 단순히 클래스 앞에 어노테이션을 붙이기만하면 스키마가 생성된다. 사용자의 custom Value 타입을 자연스럽게 스키마의 attributes 중 하나로 채택할 수 있다. (여기에는 기본 Value 타입인 String, Int, Float 등도 포함되며, 복잡한 Value 타입인 Struct, Enum, Codable, Collections of Value types 등이 모두 포함된다.)

 

SwiftData의 Model은 Reference 타입을 데이터베이스의 Relationship으로 취급한다. '다른 모델 타입' 혹은 '다른 모델 타입의 Collection'으로 모델간의 Relationship을 만들 수 있다. (1:1, 1:N)

 

몇가지 Attributes를 사용해 SwiftData가 스키마를 어떻게 형성할지 결정할 수 있다.

  • @Attributes 를 통해 uniqueness constraints 부여가능
  • @Relationship 을 통해 역을 선택하고(control the cohice of inverses) 삭제 전파방법을 결정할 수 있음
  • @Transient 를 통해 특정 프로퍼티를 무시할 수 있음
@Model
class Trip {
    @Attribute(.unique) var name: String
    var destination: String
    var endDate: Date
    var startDate: Date

    @Relationship(.cascade) var bucketList: [BucketListItem]? = []
    var livingAccommodation: LivingAccommodation?
}
  • name은 unique하게 설정
  • 현재 Trip이 삭제되었을 때 연관된 bucketList의 모든 데이터가 삭제되도록 설정

SwiftData의 모델링에 대해서 더 자세히 알고 싶으면 Model your schema with SwiftData 세션 참고

Working with your data

ModelContainer, ModelContext

데이터를 관리할 때 주로 사용될 ModelContainer와 ModelContext에 대해 알아보자.

 

ModelContainer는 내 모델 타입들의 영구 백엔드 역할을 한다. 그냥 기본으로 사용할 수도 있고, 설정이나 마이그레이션 옵션을 주고 커스텀도 가능하다. 그냥 사용하고 싶은 모델 타입들의 리스트를 지정하는 것만으로 간단하게 Model Container를 생성할 수 있다. 필요한 경우 ModelConfiguration으로 옵션을 부여한다.

// Initialize with only a schema
let container = try ModelContainer([Trip.self, LivingAccommodation.self])

// Initialize with configurations
let container = try ModelContainer(
    for: [Trip.self, LivingAccommodation.self],
    configurations: ModelConfiguration(url: URL("path"))
)

SwiftUI 환경에서도 모델 컨테이너를 설정하여 자동으로 뷰의 환경에 설정되도록 할 수도 있다.

@main
struct TripsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(
            for: [Trip.self, LivingAccommodation.self]
        )
    }
}

어쨌든 이렇게 ModelContainer를 생성하고나면 이제 Model Context를 활용해 데이터를 가져오고 저장하면 된다. Model Context는 모델의 모든 변화를 관찰하며, 동시에 다양한 작업이 가능하도록 도와준다. 또한 업데이트를 추적하고, 데이터를 가져오고, 변화를 저장하고, 심지어 실행을 취소할 수 있다.

 

일반적으로 SwiftUI에서는 @Enviroment 를 통해 Model Context를 가져와서 활용한다.

import SwiftUI

struct ContextView : View {
    @Environment(\\.modelContext) private var context
}

View 계층에 포함되어 있지 않은 경우 Model Container로부터 Main Actor에서만 실행되도록 한정되는 공유(싱글톤) Model Context를 받아올 수 있다. 아니면 그냥 단순히 새롭게 Model Context 객체를 만들어도 된다.

let context = container.mainContext
// or
let context = ModelConetext(container)

컨텍스트를 얻었으면 이를 활용해 데이터를 관리할 수 있다.

 

SwiftData는 새로운 Swift 기본 타입인 Predicate 와 FetchDescriptor , 기존보다 개선된 SortDescriptor 를 적극적으로 활용한다. iOS 17에서 새로 나온 Predicate 는 Swift의 기본 타입들과 함께 작동하며, #Predicate 매크로를 활용해 생성할 수 있다. 이는 type check가 가능한 NSPredicate의 대체품이다.

 

아래와 같이 작성이 가능하다.

let today = Date()
let tripPredicate = #Predicate<Trip> { 
    $0.destination == "New York" &&
    $0.name.contains("birthday") &&
    $0.startDate > today
}
  • Trip 데이터 중
  • destination이 “New York”이며
  • name에 “birthday”를 포함하고
  • startDate가 today보다 큰 미래의
  • 데이터를 요청하는 Predicate

이렇게 생성된 predicate를 FetchDescriptor 에게 전달하여 객체를 생성하고, Model Context에게 실질적으로 fetch를 수행하도록 요청한다.

let descriptor = FetchDescriptor<Trip>(predicate: tripPredicate)

let trips = try context.fetch(descriptor)

Swift 기본 타입과 keypath를 지원하도록 업데이트된 SortDescriptor 를 통해 가져오려는 데이터의 순서를 정의할 수도 있다.

let descriptor = FetchDescriptor<Trip>(
    sortBy: SortDescriptor(\\Trip.name),
    predicate: tripPredicate
)

let trips = try context.fetch(descriptor)

이 뿐만 아니라 FetchDescriptor를 기반으로 prefetch, limit 등의 옵션을 부여할 수도 있다.

Insert, Delete, Save, Change

SwiftData는 Insert, Delete, Save, Change 작업도 간편하다. 일반적인 Swift Class의 객체를 생성하는 것처럼 Model 객체를 생성하고 context에 이를 insert, delete하면 데이터가 추가, 삭제된다. 이렇게 생긴 변화는 context를 save해야 실질적인 영구 저장소에 반영된다.

var myTrip = Trip(name: "Birthday Trip", destination: "New York")

// Insert a new trip
context.insert(myTrip)

// Delete an existing trip
context.delete(myTrip)

// Manually save changes to the context
try context.save()

만약 @Model 매크로가 적용된 객체의 프로퍼티를 수정하면, context가 이를 추적하고 바로 save를 수행한다.

 

SwiftData의 Model Container와 Model Context에 대해 더 자세히 알고 싶으면 “Dive Deeper into SwiftData” 세션을 참고.

Using SwiftData with SwiftUI

SwiftData는 SwiftUI와 호환성이 좋다. Context를 가져오기도 편리하고, 수정사항을 알아서 저장해준다. 앞서 살펴봤듯이 .modelContainer modifier로 쉽게 container를 가져오고 @Environment 를 기반으로 context도 쉽게 불러올 수 있다.

  • @Query 프로퍼티 래퍼를 사용하면 간단하게 sort, filter 처리를 거친 후의 데이터를 받아올 수 있다.
  • @Published 옵션 없이도 내부 프로퍼티에 변경사항이 생길 경우 이를 관찰하여 SwiftUI가 뷰를 새로 불러온다.
import SwiftUI

struct ContentView: View  {
    @Query(sort: \\.startDate, order: .reverse) var trips: [Trip]
    @Environment(\\.modelContext) var modelContext

    var body: some View {
       NavigationStack() {
          List {
             ForEach(trips) { trip in 
                 // ...
             }
          }
       }
    }
}

더 자세한 내용을 알고 싶으면 “Build an app with SwiftData” 세션 참고

정리

  • SwiftData는 Swift 기본 기능을 적극적으로 활용하는 데이터 관리 도구이다.
  • 특히 SwiftUI랑 사용하기 좋은 데이터 관리 도구다.
  • Swift의 새로운 macro system를 사용한다.
  • @Model 매크로로 쉽게 스키마를 만들 수 있다.
  • 쉽게 영구저장소를 생성하고, 작업 취소 및 재실행, iCloud 동기화, Widget 개발 등에 사용할 수 있다.
반응형