iosswiftswiftuiobservableobject

Solving warning "Publishing changes from within view updates is not allowed, this will cause undefined behavior." in SwiftUI


I am getting the following purple warning in my SwiftUI view update:

  "Publishing changes from within view updates is not allowed, this will cause undefined behavior."

The warning is caused due to the following in my ObservableObject code:

    fileprivate func set(_ value: Any?, forKey key: Key) {
    
           if peerDeviceSettings != nil {
               peerDeviceSettings?[key.rawValue] = value
           } else {
               UserDefaults.standard.set(value, forKey: key.rawValue)
           }

             objectWillChange.send() //<--- This line causes warning
      }

     var someProperty:Array<MyPropertyType> {
    
       get {
           var allValues:Array<MyPropertyType > = []
        
           if let storedValues = setting(forKey: Key.myValues) as? Array<[String:Any]> {
                 allValues = storedValues.map { MyPropertyType($0) }
           } else {
               //Install default values now
               let allValuesDictionary = MyPropertyType.defaultValuesArray()
               set(allValuesDictionary, forKey: Key.myValues)
               allValues = self.someProperty
            }
        
            return allValues
        }
    
        set (newValue) {
            var dictionaries:Array<Dictionary<String, Any>> = []
            dictionaries = newValue.map { $0.dictionary() }
            set(dictionaries, forKey: Key.myValues)
         }
      }

So basically I am reading a setting property from body var and the settings code is trying to install default values if nothing was found at the startup and the setter code is triggering objectWillChange.send() . To solve it, I have modified the setter code to notify changes on the main queue.

  fileprivate func set(_ value: Any?, forKey key: Key) {
     if peerDeviceSettings != nil {
         peerDeviceSettings?[key.rawValue] = value
     } else {
         UserDefaults.standard.set(value, forKey: key.rawValue)
     }
    
     DispatchQueue.main.async {
         self.objectWillChange.send() //<-- This fixes the warning
     }
  }

I want to know if this solution has any pitfalls or there is a better solution?


Solution

  • Will it work? Yes.

    Are there any pitfalls? Yes. You are setting a value in a get function, which breaks the Principle Of Least Astonishment.

    Is there a better solution? Probably, depending on what you are trying to do.

    As @jnpdx mentioned, you could use @AppStorage in the view itself. You could also give back a default value in the read function without setting the underlying value itself. You could set the default value on appear, or segregate this matter completely from the ViewModel, so that a provider of this collection is injected and subscribed to.

    Ask yourself: who owns this model? At what stage should it be initiated with default values? Your code should reflect the answer to these questions.