1. 내용 캡쳐
- 프로젝트를 진행하면서 SwiftData를 사용하게 되었는데, 삽입된 데이터 값들을 여러 뷰에서 사용해야 했고 각각의 뷰에서 @Query를 실행해서 데이터를 불러오는 것이 아닌, 데이터를 관리하는 하나의 싱글톤 객체가 있으면 어떨까?라고 생각해보았다. ObservableObject로 선언된 Class를 생성하고 클래스 내부에서 @Query를 사용해서 SwiftData에 저장된 데이터를 가져오고, @Published로 선언된 객체를 사용해서 데이터를 관리하고자 하였다. 하지만 @Query를 사용해서 데이터를 불러온 결과값이 들어오지 않는다는 것을 알게 되었다.
2. 원인 분석
- 불가능한 이유를 알아보고자 찾아봤지만, 같은 방식을 사용하거나 공식 문서에서 명확한 답을 얻을 수 없어 Apple Developer Forums에 글을 쓰게 되었다.
https://developer.apple.com/forums/thread/755896
Apple DTO Engineer의 답변을 기반으로 테스트를 해보았다.
우선 ModelContext를 View외부에서 사용해서 insert()를 실행해보았더니, 아래와 같은 오류를 발견하게 되었다. Accessing Environment<ModelContext>'s value outside of being installed on a View. This will always read the default value and will not update.
뷰에 설치되지 않은 환경<ModelContext>의 값에 액세스합니다. 이는 항상 기본값을 읽고 업데이트되지 않습니다.
간과한 사실이 있었으니, @Environment 자체가 EnvironmentValues의 특정 요소를 읽어와 뷰 구성에 반영하는 역할을 하는 변수인데 View가 아닌 곳에서 사용을 하니 View를 변경해주지 못하는 문제가 발생하는 것이다.
3. 결과
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@EnvironmentObject var testStore : TestStore
@State private var num : Int = 0
@Query var tests: [Test]
var body: some View {
VStack {
Button(action: {
num = num + 1
let testData = Test(id: UUID().uuidString, name: "테스트", num: num)
testStore.makeData(testData: testData, testContext: modelContext)
}, label: {
Text("데이터 넣기")
})
ForEach(tests, id: \.self) { t in
HStack {
Text("id : \(t.id)")
Text("name: \(t.name)")
Text("number : \(t.num)")
}
}
}
.padding()
}
}
class TestStore : ObservableObject {
// @Environment(\.modelContext) var modelContext
// @Query var tests: [Test]
@Published var myData : [TestIn] = []
@Published var myTestData : [Test] = []
func makeData(testData: Test, testContext: ModelContext) {
testContext.insert(testData)
}
}
이렇게 ModelContext를 넘겨주면 작동이 되지만, 결국에는 각 뷰에서 사용 시에 데이터를 넘겨줘야 한다.
@Query도 @State처럼 모델이 변경될 때마다 업데이트된 뷰를 트리거 하는 역할을 하는데 @Environment와는 다르게 오류가 발생하지는 않는다. 하지만 정상적으로 값이 들어오지도 않는다. @Query를 사용할 수는 없지만 SwiftData 데이터를 fetch하기 위해서 아래와 같은 방법으로 View 바깥에서 데이터를 불러올 수 있고, 관리할 수 있다.
class TestStore : ObservableObject {
// @Environment(\.modelContext) var modelContext
// @Query var tests: [Test]
@Published var myTestData : [Test] = []
func makeData(testData: Test, testContext: ModelContext) {
testContext.insert(testData)
}
func read(using modelContext: ModelContext) /*throws -> [Test]*/ {
do {
self.myTestData = try modelContext.fetch(FetchDescriptor<Test>())
} catch {
print("error")
}
}
}
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@EnvironmentObject var testStore : TestStore
@State private var num : Int = 0
@Query var tests: [Test]
var body: some View {
VStack {
Button(action: {
num = num + 1
let testData = Test(id: UUID().uuidString, name: "테스트", num: num)
testStore.makeData(testData: testData, testContext: modelContext)
testStore.read(using: modelContext)
}, label: {
Text("데이터 넣기")
})
ForEach(testStore.myTestData) { t in
HStack {
Text("id : \(t.id)")
Text("name: \(t.name)")
Text("number : \(t.num)")
}
}
}
.padding()
}
}
어떤 방식으로 뷰에서 데이터가 사용되는지에 따라서 적절한 방식을 고민해보고 사용하면 좋을 것 같다고 생각했다.
4. 참고
https://velog.io/@horus222128/SwiftData%EC%9D%98-Query
https://velog.io/@debby_/SwiftData-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-2