Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

After receiving update from CloudKit SwiftUI doesn't update child view, but does update if child view is inlined

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:

  1. SwiftUI only tracks changes and updates views when the view uses object property, like 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

  1. Still can't understand why view which does reference the object property won't update.
// 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
    }
}
like image 636
Yury Kaspiarovich Avatar asked Nov 30 '25 16:11

Yury Kaspiarovich


1 Answers

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.

like image 53
fasoh Avatar answered Dec 04 '25 00:12

fasoh



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!