How can I call "willChangeValue" when using swift Task/await without the following warning showing up?
Instance method 'willChangeValue' is unavailable from asynchronous contexts; Only notify of changes to a key in a synchronous context. Notifying changes across suspension points has undefined behavior.; this is an error in Swift 6
@objc dynamic var localFilesTitle: String {
get {
return "\(localTitle)(\(localFiles.count))"
}
set {
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Task {
await initialise()
self.isInitialised = true
let local = await self.getLocalFiles()
DebugLog("Found \(local.count) local files")
for file in local.filter({!$0.isDirectory}) {
DebugLog(" \(file.name),\(file.size),\(file.modifiedDate)")
}
self.willChangeValue(forKey: "localFilesTitle")
self.localFiles.append(contentsOf: local.filter({!$0.isDirectory}))
self.didChangeValue(forKey: "localFilesTitle")
// let remote = await self.getRemoteFiles()
//
// self.awsFiles = remote
}
}
CodePudding user response:
That code should be called on the main actor, so you should wrap it like this:
await MainActor.run {
self.willChangeValue(forKey: "localFilesTitle")
self.localFiles.append(contentsOf: local.filter({!$0.isDirectory}))
self.didChangeValue(forKey: "localFilesTitle")
}
CodePudding user response:
The proposed solution by jrturton works, but if you want to avoid having even more nested blocks you can also delegate to a MainActor
async
method like this:
Updated with feedback from jrturton.
override func viewDidLoad() {
super.viewDidLoad()
Task {
/*
async code here
*/
await myMethod()
}
}
@MainActor private func myMethod() {
willChangeValue(forKey: "localFilesTitle")
localFiles.append(contentsOf: local.filter({!$0.isDirectory}))
didChangeValue(forKey: "localFilesTitle")
}
CodePudding user response:
I avoid patterns where I have to call willChangeValue
and didChangeValue
manually. The dynamic
stored properties can do this for us.
So, there are a few additional approaches, in addition to those already discussed:
I would forego the computed property, make it a simple stored property, and
dynamic
will take care of all the necessary KVO notifications for me.class ViewController: NSViewController { // or UIViewController, as appropriate @objc dynamic var localFilesTitle: String = "" var localTitle: String = "" var localFiles: [FileWrapper] = [] { didSet { localFilesTitle = "\(localTitle) (\(localFiles.count))" } } override func viewDidLoad() { super.viewDidLoad() Task { localTitle = "Foo" let localDirectories = await self.getLocalFiles() .filter { $0.isDirectory } localFiles.append(contentsOf: localDirectories) } } }
The other approach is to make
localFiles
adynamic
property, as well, and usekeyPathsForValuesAffectingValue
to tell the KVO system that thelocalFilesTitle
is affected automatically bylocalFiles
:class ViewController: NSViewController { @objc dynamic var localFilesTitle: String { "\(localTitle) (\(localFiles.count))" } var localTitle: String = "" @objc dynamic var localFiles: [FileWrapper] = [] override func viewDidLoad() { // same as above ... } override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> { guard key == #keyPath(localFilesTitle) else { return super.keyPathsForValuesAffectingValue(forKey: key) } return [#keyPath(localFiles)] } }