Swift - How to use KVO's 'observe' method on protocol

Viewed 223

I need an observable property on certain view controllers in my application, I'm trying to standardize this through a protocol.

protocol MyProtocol: UIViewController {
    dynamic var observeMe: Bool { get set }
}

But when trying to observe the property like so:

let viewController = UIViewController()
if let observableViewController = viewController as? MyProtocol {
    observableViewController.observe(\.observeMe, options: [.old, .new], changeHandler: { object, change in
        // Do something
    })
}

Xcode throws the compiler error:

"Member 'observe' cannot be used on value of protocol type 'MyProtocol'; use a generic constraint instead"

2 Answers

You can declare a function and apply the generic constraint to it.

In Swift 5.7 you can simply do:

func observeViewController(_ vc: some MyProtocol) {
  vc.observe(\.observeMe, options: [.old, .new], changeHandler: { object, change in
  })
}

Then you can call it:

let viewController = UIViewController()
if let observableViewController = viewController as? MyProtocol {
  observeViewController(viewController)
}

Prior to Swift 5.7, some MyProtocol is not supported and you need to use type erasure:

class AnyMyProtocol: MyProtocol & UIViewController {
  var observeMe: Bool

  private var base: MyProtocol

  init(_ base: MyProtocol) {
    self.base = base
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

func observeViewController(_ vc: MyProtocol) {
  vc.observe(\.observeMe, options: [.old, .new], changeHandler: { object, change in
  })
}

And call it like so:

let viewController = UIViewController()
observeViewController(vc: AnyMyProtocol(base: observableViewController))

By the way, you will receive a warning reading:

Passing reference to non-'@objc dynamic' property 'observeMe' to KVO method 'observe(_:options:changeHandler:)' may lead to unexpected behavior or runtime trap

But in your case you can't declare it as @objc because an @objc protocol can't inherit from a non-protocol type.

You can try this -

import Foundation

protocol MyProtocol {
    dynamic var observeMe: Bool { get set }
}

class Test : NSObject, MyProtocol {
    @objc var observeMe: Bool = true
    private var observeMeObservation: NSKeyValueObservation?
    
    override init() {
        super.init()
        
        observeMeObservation = self.observe(\.observeMe, changeHandler: { [weak self] (_, _) in
            guard let self = self else { return }
            print("observeMe : \(self.observeMe)")
        })
    }
    
    func doSomeChanges() {
        self.observeMe = false
        self.observeMe = true
    }
    
    deinit {
        observeMeObservation?.invalidate()
    }
}

let test = Test()
test.doSomeChanges()
Related