Adding observer for KVO without pointers using Swift
Asked Answered
B

5

28

In Objective-C, I would normally use something like this:

static NSString *kViewTransformChanged = @"view transform changed";
// or
static const void *kViewTransformChanged = &kViewTransformChanged;

[clearContentView addObserver:self
                       forKeyPath:@"transform"
                          options:NSKeyValueObservingOptionNew
                          context:&kViewTransformChanged];

I have two overloaded methods to choose from to add an observer for KVO with the only difference being the context argument:

 clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, context: CMutableVoidPointer)
 clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, kvoContext: KVOContext)

With Swift not using pointers, I'm not sure how to dereference a pointer to use the first method.

If I create my own KVOContext constant for use with the second method, I wind up with it asking for this:

let test:KVOContext = KVOContext.fromVoidContext(context: CMutableVoidPointer)

EDIT: What is the difference between CMutableVoidPointer and KVOContext? Can someone give me an example how how to use them both and when I would use one over the other?

EDIT #2: A dev at Apple just posted this to the forums: KVOContext is going away; using a global reference as your context is the way to go right now.

Blue answered 12/6, 2014 at 2:35 Comment(2)
are you asking how to create a CMutableVoidPointer?Drysalter
I've edited my post to be more precise.Blue
U
16

Now that KVOContext is gone in Xcode 6 beta 3, you can do the following. Define a global (i.e. not a class property) like so:

let myContext = UnsafePointer<()>()

Add an observer:

observee.addObserver(observer, forKeyPath: …, options: nil, context: myContext)

In the observer:

override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafePointer<()>) {
    if context == myContext {
        …
    } else {
        super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }
}
Upwards answered 8/7, 2014 at 8:30 Comment(1)
UnsafePointer<()>() is a null pointer. Using a null pointer as the context does not give you a particularly reliably unique value. (I realize this is out of date nowadays — so is my answer — just thought this should be mentioned for completeness.)Seavir
S
58

There is now a technique officially recommended in the documentation, which is to create a private mutable variable and use its address as the context.

(Updated for Swift 3 on 2017-01-09)

// Set up non-zero-sized storage. We don't intend to mutate this variable,
// but it needs to be `var` so we can pass its address in as UnsafeMutablePointer.
private static var myContext = 0
// NOTE: `static` is not necessary if you want it to be a global variable

observee.addObserver(self, forKeyPath: …, options: [], context: &MyClass.myContext)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
    if context == &myContext {
        …
    }
    else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
    }
}
Seavir answered 21/9, 2015 at 17:34 Comment(0)
U
16

Now that KVOContext is gone in Xcode 6 beta 3, you can do the following. Define a global (i.e. not a class property) like so:

let myContext = UnsafePointer<()>()

Add an observer:

observee.addObserver(observer, forKeyPath: …, options: nil, context: myContext)

In the observer:

override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafePointer<()>) {
    if context == myContext {
        …
    } else {
        super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }
}
Upwards answered 8/7, 2014 at 8:30 Comment(1)
UnsafePointer<()>() is a null pointer. Using a null pointer as the context does not give you a particularly reliably unique value. (I realize this is out of date nowadays — so is my answer — just thought this should be mentioned for completeness.)Seavir
F
5

Swift 4 - observing contentSize change on UITableViewController popover to fix incorrect size

I had been searching for an answer to change to a block based KVO because I was getting a swiftlint warning and it took me piecing quite a few different answers together to get to the right solution. Swiftlint warning:

Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. (block_based_kvo).

My use case was to present a popover controller attached to a button in a Nav bar in a view controller and then resize the popover once it's showing - otherwise it would be too big and not fitting the contents of the popover. The popover itself was a UITableViewController that contained static cells, and it was displayed via a Storyboard segue with style popover.

To setup the block based observer, you need the following code inside your popover UITableViewController:

// class level variable to store the statusObserver
private var statusObserver: NSKeyValueObservation?

// Create the observer inside viewWillAppear
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    statusObserver = tableView.observe(\UITableView.contentSize,
        changeHandler: { [ weak self ] (theTableView, _) in self?.popoverPresentationController?.presentedViewController.preferredContentSize = theTableView.contentSize
        })
}

// Don't forget to remove the observer when the popover is dismissed.
override func viewDidDisappear(_ animated: Bool) {
    if let observer = statusObserver {
        observer.invalidate()
        statusObserver = nil
    }

    super.viewDidDisappear(animated)
}

I didn't need the previous value when the observer was triggered, so left out the options: [.new, .old] when creating the observer.

Foin answered 8/7, 2018 at 5:18 Comment(0)
G
3

Update for Swift 4

Context is not required for block-based observer function and existing #keyPath() syntax is replaced with smart keypath to achieve swift type safety.

class EventOvserverDemo {
var statusObserver:NSKeyValueObservation?
var objectToObserve:UIView?

func registerAddObserver() -> Void {
    statusObserver = objectToObserve?.observe(\UIView.tag, options: [.new, .old], changeHandler: {[weak self] (player, change) in
        if let tag = change.newValue {
            // observed changed value and do the task here on change.
        }
    })
}

func unregisterObserver() -> Void {
    if let sObserver = statusObserver {
        sObserver.invalidate()
        statusObserver = nil
    }
  }
}
Gavotte answered 4/7, 2018 at 8:47 Comment(1)
The question is about the context API, which by the way supports subclassing where as the block-based does not.Amyloid
F
-1

Complete example using Swift:

//
//  AppDelegate.swift
//  Photos-MediaFramework-swift
//
//  Created by Phurg on 11/11/16.
//
//  Displays URLs for all photos in Photos Library
//
//  @see https://mcmap.net/q/503089/-programmatic-access-to-the-photos-library-on-mac-os-x-photokit-photos-framework-for-mac
//

import Cocoa
import MediaLibrary

// For KVO: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID12
private var mediaLibraryLoaded = 1
private var rootMediaGroupLoaded = 2
private var mediaObjectsLoaded = 3

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!
    var mediaLibrary : MLMediaLibrary!
    var allPhotosAlbum : MLMediaGroup!


    func applicationDidFinishLaunching(_ aNotification: Notification) {

        NSLog("applicationDidFinishLaunching:");

        let options:[String:Any] = [
            MLMediaLoadSourceTypesKey: MLMediaSourceType.image.rawValue, // Can't be Swift enum
            MLMediaLoadIncludeSourcesKey: [MLMediaSourcePhotosIdentifier], // Array
        ]

        self.mediaLibrary = MLMediaLibrary(options:options)
        NSLog("applicationDidFinishLaunching: mediaLibrary=%@", self.mediaLibrary);

        self.mediaLibrary.addObserver(self, forKeyPath:"mediaSources", options:[], context:&mediaLibraryLoaded)
        NSLog("applicationDidFinishLaunching: added mediaSources observer");

        // Force load
        self.mediaLibrary.mediaSources?[MLMediaSourcePhotosIdentifier]

        NSLog("applicationDidFinishLaunching: done");

    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        NSLog("observeValue: keyPath=%@", keyPath!)
        let mediaSource:MLMediaSource = self.mediaLibrary.mediaSources![MLMediaSourcePhotosIdentifier]!

        if (context == &mediaLibraryLoaded) {
            NSLog("observeValue: mediaLibraryLoaded")
            mediaSource.addObserver(self, forKeyPath:"rootMediaGroup", options:[], context:&rootMediaGroupLoaded)
            // Force load
            mediaSource.rootMediaGroup

        } else if (context == &rootMediaGroupLoaded) {
            NSLog("observeValue: rootMediaGroupLoaded")
            let albums:MLMediaGroup = mediaSource.mediaGroup(forIdentifier:"TopLevelAlbums")!
            for album in albums.childGroups! {
                let albumIdentifier:String = album.attributes["identifier"] as! String
                if (albumIdentifier == "allPhotosAlbum") {
                    self.allPhotosAlbum = album
                    album.addObserver(self, forKeyPath:"mediaObjects", options:[], context:&mediaObjectsLoaded)
                    // Force load
                    album.mediaObjects
                }
            }

        } else if (context == &mediaObjectsLoaded) {
            NSLog("observeValue: mediaObjectsLoaded")
            let mediaObjects:[MLMediaObject] = self.allPhotosAlbum.mediaObjects!
            for mediaObject in mediaObjects {
                let url:URL? = mediaObject.url
                // URL does not extend NSObject, so can't be passed to NSLog; use string interpolation
                NSLog("%@", "\(url)")
            }
        }
    }

}
Farthingale answered 11/11, 2016 at 19:45 Comment(1)
Can you explain this complete example?Kamalakamaria

© 2022 - 2024 — McMap. All rights reserved.