skip to Main Content

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


  1. 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()
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search