SwiftData 사용할 때 변경 시점도 업데이트 하는 방법
상황설명
- swiftData를 사용하여 데이터 모델링
@Model
public final class SceneCard {
// MARK: - Properties
@Attribute(.unique) public var id: String
public var title: String
public var order: Int
public var summary: String
public var subtitle: String
public var done: Bool
public var createdAt: Date
public var modifiedAt: Date
...
-@Bindable을 사용해 모델의 프로퍼티를 직접 TextEditor에 바인딩함
struct SceneCardView: View {
@Bindable let scene: SceneCard
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Header area
HStack {
TextEditor(text: $scene.title)
위와 같이 구성하면 SwiftData의 모델은 context에서 관리되고 있기 때문에 @Bindable로 바인딩된 모델의 프로퍼티가 변경되었을 때 이를 context 내부에 체크에 두었다가 내부적으로 commit(or flush)가 나가는 시점에 자동으로 db에 업데이트가 됩니다.
처음에는 SwiftData의 모델 업데이트 자동관리를 사용하면 편리하게 CRUD를 할 수 있겠지라고 생각하며 위와 같이 구조를 설계했습니다. 따로 업데이트 로직을 호출해 줄 필요도 없고 자동으로 관리를 해준다고 개꿀인데?
하지만 세상 만사 마음대로 되는 게 없더군요.
하고 싶은 것 : SwiftData 모델의 프로퍼티가 변경된 시점에 modifiedAt 프로퍼티도 업데이트를 해주고 싶음
문제 사항
1. SwiftData의 @Model 모델은 변경이 일어났을 때 이를 자동으로 감지했다가 지가 원하는 타이밍에 db에 커밋함, 근데 이 시점이 명확히 언제지 모르겠음, 그래서 찾아보니까 "뷰 생명주기, 런 루프에 따라 자동으로 업데이트됨 걱정 ㄴㄴ"라고 하는데 실제로 RUN을 해보면 저장이 될 때도 있고 안될 때도 있고 시점이 불명확함
2. 그러면 자동으로 업데이트가 반영되는 시점에 원하는 행동을 같이 해주는 (willSet, didSet 같은) 것들을 적용하고 싶은데 SwiftData의 @Model 매크로가 저장 프로퍼티 접근을 관리하며 내부적으로 프로퍼티 접근을 재정의하여 모델의 프로퍼티에 대해서는 willSet, didSet 옵저버가 사용 불가함.
3. 값 변경을 위한 별도의 연산프로퍼티를 만들고 이를 통해 변경을 하기에는 연산 프로퍼티는 내부적으로 저장하고 있는 값이 없기 때문에 @Bindable 을 이용한 UI에 바인딩이 불가함
4. context의 willSet 노티를 notificationCenter를 통해 받아서 이를 감지해 modifiedAt을 업데이트하면 이 또한 변경 감지가 되어 순환 사이클이 형성됨
5. @Transient 프로퍼티를 사용하고 didSet, willSet를 정의해 이를 통해 프로퍼티 값 변경 및 변경시점 감지와 저장을 한다.
마지막 방법을 시도하려고 구조를 짜보니까 텍스트 필드 특성상 입력으로 인한 변경이 아주 많이 발생하는데, 그러면 텍스트 변경이 있을 때마다 프로퍼티 업데이트를 시도하고 또 이때마다 db에 자동으로 commit을 한다???? 일반 String을 쓴다면, 애플이 잘 만들어 놓았겠지라고 생각하면서 사용했겠지만, 향후에 리치 텍스트 제공을 위해 NSAttributedString을 사용할 것이고 해당 타입은 SwiftData가 지원하지 않는 타입이기 때문에 ValueTransformer를 사용해 저장을 할 때마다 Data로 컨버팅을 해줘야 하는데 매 입력마다 자동으로 저장을 해버리면 버벅거리는 게 눈에 선했기 때문에 다른 방법을 선택했습니다.
선택한 방법 : 뷰 단에 상태 관리 + 명시적인 save 호출 + combine으로 조절
일단 언제 되는지 제대로 설명도 안 해주는 자동 저장 옵션을 껐습니다. 젠장 이럴거면 그냥 coredata 쓸껄
self.container.mainContext.autosaveEnabled = false
컴포넌트 내부에서 현재 입력되는 텍스트를 관리하도록 @State 프로퍼티로 뷰에서 상태관리를 따로 해줍니다. 그리고 onChange 모디파이어를 사용해 해당 프로퍼티에 변경 시 전달받은 클로저를 통해 Combine으로 변경 사항을 발행하도록 합니다.
struct SceneCardView: View {
let scene: SceneCard
let onPropertyChanged: (String, Any) -> Void
// Local state for UI only
@State private var titleText: String
// Initialize with current values
init(scene: SceneCard, onPropertyChanged: @escaping (String, Any) -> Void) {
self.scene = scene
self.onPropertyChanged = onPropertyChanged
_titleText = State(initialValue: scene.title)
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Header area
HStack {
TextEditor(text: $titleText)
.onChange(of: titleText) { _, newValue in
onPropertyChanged("title", newValue)
}
...
마지막으로 해당 컴포넌트를 사용하는 부모 뷰에서 발행된 변경사항을 구독한 다음 debouce를 통해 더 이상 변경이 일어나지 않은 시점에 명시적으로 save를 호출합니다. 이러면 각 입력마다 저장을 호출하지 않아 효율적인 저장이 가능합니다.
struct ScriptModePanel: View {
var draft: Draft
let onAddScene: () -> Void
// State for tracking scene changes
@State private var changePublisher = PassthroughSubject<(SceneCard, String, Any), Never>()
@State private var cancellables = Set<AnyCancellable>()
var body: some View {
List {
ForEach(sortedScenes) { scene in
SceneCardView(
scene: scene,
onPropertyChanged: {(property, value) in
changePublisher.send((scene, property, value))
}
)
...
.onAppear {
setupPublishers()
}
.onDisappear {
cancellables.removeAll()
}
}
private func setupPublishers() {
cancellables.removeAll()
changePublisher
.debounce(for: 0.5, scheduler: RunLoop.main)
.sink { (scene, property, value) in
do {
try scene.updateProperty(property, value: value)
} catch {
print("Failed to update scene property: \(error)")
}
}
.store(in: &cancellables)
}
}
정리해 보니까 뭐 별거 없는 것 같아서 조금 허탈하기도 하지만 해결 과정에서 깨달은 게 있습니다. "새롭다고 다 좋은 건 아니다." 가뜩이나 iOS 커뮤니티도 그렇게 크지 않은데 신생아 수준의 프레임워크라 자료도 많이 없어서 관련된 내용은 거의 다 읽어본 것 같습니다. 조금 더 성숙해질 때까지는 CoreData를 애용할 것 같습니다.
- SwiftData 자동 업데이트 시점 : https://www.hackingwithswift.com/quick-start/swiftdata/when-does-swiftdata-autosave-data
- autosaveEnabled 옵션
autosaveEnabled | Apple Developer Documentation
A Boolean value that indicates whether the context should automatically save any pending changes when certain events occur.
developer.apple.com
- SwiftData의 @Model에 프로퍼티 감시자 사용법
SwiftData Model didset | Apple Developer Forums
It's somewhat I finally done in my code. But, for this to work, it's mandatory to use @Transient macro to tell Xcode the property is not persistent. This approach involves to "double" some properties, as you've done in your example. So, for your solution t
developer.apple.com
- SwiftData 모델에 modifiedAt 속성 업데이트 방법
What is the best SwiftData approach to implement a modified date
When using Core Data I would override willSave on NSManagedObject to compute a lastModified value on a model object. This allowed one simple method to check changed values and set a date if necessa...
stackoverflow.com