Search code examples
swiftgenericsprotocolsswift5

Generic protocol for observing changes in Swift 5


Trying hard to code in Swift 5 the Java example below.

Generally, I want to have an Observable protocol which will be adopted by multiple other protocols. I need these protocols to be types in functions' arguments, so that these functions can add additional observers.

In Java, it is very easy to do. The code prints out:

Observer 1 changed to 10
Observer 2 changed to 10

,

interface Observable<O> {
    void addObserver(O observer);
}

interface Settings extends Observable<SettingsObserver> {
    void setInterval(int interval);
}

interface SettingsObserver {
    void intervalChanged(int interval);
}

class AppSettings implements Settings {
    private List<SettingsObserver> observers = new ArrayList<>();

    @Override public void addObserver(SettingsObserver observer) { observers.add(observer); }
    @Override public void setInterval(int interval) { observers.forEach(observer -> observer.intervalChanged(interval)); }
}

class Observer1 implements SettingsObserver {
    @Override public void intervalChanged(int interval) {
        System.out.println("Observer 1 changed to " + interval);
    }
}

class Observer2 implements SettingsObserver {
    @Override public void intervalChanged(int interval) {
        System.out.println("Observer 2 changed to " + interval);
    }
}

class Main {
    public static void main(String[] args) {
        Observer1 observer1 = new Observer1();

        Settings settings = new AppSettings();
        settings.addObserver(observer1);

        Main main = new Main();
        main.run(settings);
    }

    void run(Settings settings) {
        Observer2 observer2 = new Observer2();
        settings.addObserver(observer2);

        settings.setInterval(10);
    }
}

Solution

  • While it's simple to create a generic wrapper to which you can add your own observables, there are two native solutions that you should use instead.

    1. Notifications.

      When value is changed, send a notification using NotificationCenter.default. Observers should listen to these notifications. Notification are a crucial part of the ecosystem:

      class AppSettings {
          enum Notifications {
              static let intervalChanged = Notification.Name("AppSettingsIntervalChangedNotification")
          }
      
          var interval: TimeInterval = 0 {
              didSet {
                  NotificationCenter.default.post(name: Notifications.intervalChanged, object: self)
              }
          }
      }
      
      let settings = AppSettings()
      let observer = NotificationCenter.default.addObserver(
          forName: AppSettings.Notifications.intervalChanged,
          object: settings,
          queue: nil
      ) { [weak settings] _ in
          guard let settings = settings else { return }
          print(settings.interval)
      }
      
      settings.interval = 10
      
    2. Key-value observing (KVO)

      If you inherit your objects from NSObject, you can simply add a direct observer to any Obj-C compatible value:

      class AppSettings: NSObject {
          @objc dynamic var interval: TimeInterval = 0
      }
      
      let settings = AppSettings()
      
      let observer: NSKeyValueObservation = settings.observe(\.interval, options: .new) { _, change in
          print(change.newValue)
      }
      
      settings.interval = 10
      

      See https://developer.apple.com/documentation/swift/cocoa_design_patterns/using_key-value_observing_in_swift

    Just for completeness a simple generic observer here:

    class Observable<ValueType> {
        typealias Observer = (ValueType) -> Void
    
        var observers: [Observer] = []
        var value: ValueType {
            didSet {
                for observer in observers {
                    observer(value)
                }
            }
        }
    
        init(_ defaultValue: ValueType) {
            value = defaultValue
        }
    
        func addObserver(_ observer: @escaping Observer) {
            observers.append(observer)
        }
    }
    
    class AppSettings {
        let interval: Observable<TimeInterval> = Observable(0)
    }
    
    let settings = AppSettings()
    settings.interval.addObserver { interval in
        print(interval)
    }
    settings.interval.value = 10
    

    Note that all my observers are simple closures. The reason why Java uses objects as observers is mostly historical due to Java limitations. There is no need for Observable or Observer protocols in Swift.