Marmelador
Marmelador

Reputation: 1017

Add property observer to global variable inside class in Swift

I have a variable globalVariable declared at global scope that may change at any time.

Different ViewControllers in my app need to react differently, when globalVariable changes.

Thus it would be desirable to add a property observer in each ViewController that execute the needed code when globalVariable changes.

I cannot seem to achieve it with override or extension. What is the way to go here?

Upvotes: 4

Views: 4288

Answers (2)

Rob
Rob

Reputation: 437592

If your goal is to simply know when your global variable changed, you could have it post a notification upon change:

extension NSNotification.Name {
    static let globalVariableChanged = NSNotification.Name(Bundle.main.bundleIdentifier! + ".globalVariable")
}

var globalVariable: Int = 0 {
    didSet {
        NotificationCenter.default.post(name: .globalVariableChanged, object: nil)
    }
}

Then any object can add an observer for that notification:

class ViewController: UIViewController {

    private var observer: NSObjectProtocol!

    override func viewDidLoad() {
        super.viewDidLoad()

        // add observer; make sure any `self` references are `weak` or `unowned`; obviously, if you don't reference `self`, that's not necessary

        observer = NotificationCenter.default.addObserver(forName: .globalVariableChanged, object: nil, queue: .main) { [weak self] notification in
            // do something with globalVariable here
        }
    }

    deinit {
        // remember to remove it when this object is deallocated

        NotificationCenter.default.removeObserver(observer)
    }

}

Note, this didSet mechanism will not detect changes if (a) the global variable is a reference type, i.e. a class; and (b) it merely mutates the object that the global variable references rather than replacing it with a new instance. To identify that scenario, you need to use KVO or other mechanism to detect mutation.

Upvotes: 9

Alain T.
Alain T.

Reputation: 42143

There can be only one didSet{} function for your global variable and it must belong to the variable itself. What you can do is make the variable's didSet{} function call a list of functions from other objects.

You could use notifications for this or you could build your own mechanism.

Here's an example of how you could create your own mechanism:

(note that this is pretty generic and could work for any variable types or singleton instance)

// Container for an observer's function reference
// - will be used to call the observer's code when the variable is set
// - Separates the object reference from the function reference
//   to avoid strong retention cycles.
struct GlobalDidSet<T>
{
   weak var observer:AnyObject?
   var didSetFunction:(AnyObject)->(T)->()
   init(_ observer:AnyObject, didSet function:@escaping (AnyObject)->(T)->())
   {
      self.observer  = observer
      didSetFunction = function
   }
}

// Container for a list of observers to be notified
// - maintains the list of observers
// - automatically clears entries that non longer have a valid object
// - calls all observers when variable changes
// - erases type of observer to allow generic use of GlobalDidSet<>
struct GlobalDidSets<T>
{
   var observers : [GlobalDidSet<T>] = []
   mutating func register<O:AnyObject>(_ observer:O, didSet function:@escaping (O)->(T)->())
   {
      let observer = GlobalDidSet<T>(observer)
      { (object:AnyObject) in function(object as! O) }
      observers.append(observer)  
   }

   mutating func notifyDidSet(_ oldValue:T)
   {
      observers = observers.filter{$0.observer != nil}
      observers.forEach{ $0.didSetFunction($0.observer!)(oldValue) }
   }
}

...

// To use this, you will need a second variable to manage the list of observers
// and your global variable's didSet{} must use that observer list
// to perform the multiple function calls
//
var globalVariableDidSets = GlobalDidSets<String>()
var globalVariable : String = "Initial Value"
{
   didSet { globalVariableDidSets.notifyDidSet(oldValue) }
}

// In your view controllers (or any other class), you need to setup the 
// reaction to the global variable changes by registering to the observer list
//
class MyVC:UIViewController
{
    override func viewDidLoad()
    {
      globalVariableDidSets.register(self){ $0.handleVariableChange }  
      // ...
    }

    func handleVariableChange(_ oldValue:String)
    {
      //...
    }
}

Upvotes: 0

Related Questions