This works as expected after receiving an update from CloudKit:
@Model
final class File {
var name: String = "" // <- I edit the name on another device in the simple text editor, the editor code is ommited as it's not significant
init(name: String) {
self.name = name
}
}
struct ContentView: View {
@Query(sort: \File.name) private var files: [File]
var body: some View {
NavigationStack {
ForEach(files) { file in
NavigationLink(value: file) {
Text(file.name) // <- The text updates successfully after CloudKit sync
}
}
}
}
}
Here is the same app, but with extracted sub-views:
struct ContentView: View {
@Query(sort: \File.name) private var files: [File]
var body: some View {
NavigationStack {
FilesView(files: files) // <- Just wrapped the code into a separate view
}
}
}
struct FilesView: View {
var files: [File]
var body: some View {
ForEach(files) { file in
NavigationLink(value: file) {
FileTileView(file: file) // <- Another extracted view
}
}
}
}
struct FileTileView: View {
var file: File
var body: some View {
Text(file.name) // <- Doesn't get updated when CloudKit received the sync update
}
}
The code is the same, just couple of extracted views. Why doesn't it work?
Update:
My findings are:
file.name and this property changes. If view only references the file and the file reference doesn't change (which it won't) SwiftUI won't update the view // This view will only be updated if the `files` collection itself will be changed, i.e. element deleted/added to it
var body: some View {
ForEach(files) { file in
NavigationLink(value: file) {
FileTileView(file: file)
}
}
}
See this link for more info: https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
// file.name does get changed by iCloud but it won't update
var body: some View {
Text(file.name)
}
It seem to be related to iCloud changing the object without triggering notification for the SwiftUI. I assume this happens because iCould sync happens in the background thread.
Workaround so far is to pass file.persistentModelID (or any ID) and query the file:
var body: some View {
ForEach(files) { file in
NavigationLink(value: file) {
FileTileView(fileId: file.persistentModelID) // <-
}
}
}
struct FileTileView: View {
var fileId: PersistentIdentifier
@Query private var fileFilterResult: [File]
init(fileId: PersistentIdentifier) {
self.fileId = fileId
_fileFilterResult = Query(
filter: #Predicate<File> { $0.persistentModelID == fileId }
)
}
private var file: File? {
fileFilterResult.first
}
var body: some View {
Text(file.name) // <- Gets updated when CloudKit received the sync update
}
}
What I found out to work was using the attribute that can update via CloudKit in the initializer. This runs before the view is initialized and thus sets the necessary value tracking. I haven't found any documentation about this very specific behaviour but it does work (See comments):
struct FileTileView: View {
var file: File
let name: String // 1 - New attribute for every property that can update via CloudKit
init(file: File) {
self.name = file.name // 2 - Assign the object that is being updated to it
}
var body: some View {
Text(name) // 3 - Display this value instead of file.name
}
}
Upon further inspection step 3 is actually not required. You can still reference file.name as long as you use it in the initializer of the view.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With