Home > Net >  How to avoid Swift warning "Instance method 'willChangeValue' is unavailable from asy
How to avoid Swift warning "Instance method 'willChangeValue' is unavailable from asy

Time:12-03

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:

  1. 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)
            }
        }
    }
    
  2. The other approach is to make localFiles a dynamic property, as well, and use keyPathsForValuesAffectingValue to tell the KVO system that the localFilesTitle is affected automatically by localFiles:

    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)]
        }
    }
    
  • Related