4

It's "easy" to KVO a value in Swift3, using the technique in this great post...

https://stackoverflow.com/a/25219216/294884

How do I KVO the alpha of a UIView?

Goal: imagine a screen with a small view V. Screen has a large table view. Each cell has a small view CV.

I want each CV to follow1 the alpha of V, actually during an animation of the alpha of V.

(The only real alternate I can think of is to CADisplayLink the cells and poll the alpha of V, which kind of sucks.)


1Reason for that? It's the only way to sync alpha changes across cells.


Note Just as Rob explains below, in fact the alpha of a layer does not animate. (It just sets "instantly" to the new value at the beginning of an animation.) In fact you have to follow the presentation layer. Here's an example of the converse process, pulling it from where you want every draw

let d = CADisplayLink(target: self, selector: #selector(ThisClassName.updateAlpha))
d.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)

func updateAlpha() {
  let a = leader.layer.presentation()?.value(forKey: "opacity") as! CGFloat
  follower.alpha = a
  }
Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719

1 Answers1

10

This is not an ideal use-case of KVO. You'd probably prefer to go to the code that is changing the alpha and have it do the necessary updates of the other views (or build some interface to do that more generically; something other than KVO).

Also note that if you're dealing with animations, the KVO of alpha may only identify the initiation of the animation, not observing the changes mid-flight during the animation. (Those sorts of changes are generally captured with the presentation/presentationLayer in a CADisplayLink or the like.)

Having said that, yes, you can observe alpha property. So, in Swift 3, just define context:

private var observerContext = 0  

And then add the observer:

observedView.addObserver(self, forKeyPath: "alpha", options: .new, context: &observerContext)

and then implement your observer:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard context == &observerContext else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }

    // do what you want here
}

Note, remember to remove the observer. For example, you might do that in deinit:

deinit {
    observedView.removeObserver(self, forKeyPath: "alpha", context: &observerContext)
}

Note, in Swift 4, this process is simplified a bit. Define a token property to keep track of your observer:

private var token: NSKeyValueObservation?

And then observe the alpha property:

token = observedView.observe(\.alpha) { [weak self] object, change in
    // do something
}

Note the use of [weak self] to ensure the closure doesn't cause strong reference cycle. Obviously, if you're not referencing self in that closure, that is not needed.

But the virtue of the block based pattern is that (a) when the the token falls out of scope, the observer is automatically removed, so no special deinit routine is needed; (b) the observable keys for observedView are now strongly typed, avoiding simple typographical errors in the keys; and (c) because the observer is linked to this closure, we no longer have to do that check of the context of the observer and call self if this wasn't our context.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Magnificent! Of course, it's `forKeyPath: "alpha"`. Thanks so much. – Fattie Dec 29 '16 at 18:24
  • @Rob is there a key for view frame/bounds? – mojtaba al moussawi Aug 23 '18 at 14:29
  • Yep. But for that it’s often better to use `layoutView` (for `UIView` subclasses) or `viewDidLayoutSubview` (for `UIViewController` subclasses). – Rob Aug 24 '18 at 19:27
  • The Swift 4 approach is great on iOS 11 & iOS 12 but I'm seeing some crashes from this on iOS 10... researching further to see if I'm doing something else wrong or if it's actually OS-related – Tim Jan 19 '19 at 20:41
  • @Tim - iOS 12 gracefully handles if an observed object is deallocated while you still have the observer in place. iOS 10 does not. You’ll need to remove the observer before the observed object is deallocated. Perhaps that’s what’s going on in your situation. It’s hard to say in the absence of a [MCVE](https://stackoverflow.com/help/mcve) – Rob Jan 19 '19 at 21:12
  • @Rob Thanks for getting back to me and mentioning this. I think we could update this answer to clarify the OS-dependent behavior, as it's misleading to say that in Swift 4 it is sufficient for the `token` to deallocate to remove the observer; on iOS 10, you must call `token.invalidate()` in the class's `deinit` method. This works fine on iOS 11+, too. – Tim Jan 21 '19 at 02:37