Modern thread-safe and type-safe key-value observing for Swift and Objective-C

Related tags

Event PMKVObserver
Overview

Now Archived and Forked

PMKVObserver will not be maintained in this repository going forward. Please use, create issues on, and make PRs to the fork of PMKVObserver located here.

PMKVObserver

Version Platforms Languages License Carthage compatible CocoaPods

PMKVObserver provides a safe block-based wrapper around Key-Value Observing, with APIs for both Obj-C and Swift. Features include:

  • Thread-safety. Observers can be registered on a different thread than KVO notifications are sent on, and can be cancelled on yet another thread. An observer can even be cancelled from two threads simultaneously.
  • Automatic unregistering when the observed object deallocates.
    • Note: nested key paths are not supported at this time and manual cancellation should be used; more info here
  • Support for providing an observing object that is given to the block, and automatic unregistering when this observing object deallocates. This lets you call methods on self without retaining it or dealing with a weak reference.
  • Thread-safety for the automatic deallocation. This protects against receiving messages on another thread while the object is deallocating.
  • First-class support for both Obj-C and Swift, including strong typing in the Swift API.

Examples

Swift

// Observe an object for as long as the object is alive.
_ = KVObserver(object: user, keyPath: \User.fullName) { object, _, _ in
    // `object` has the same type as `user`
    print("User's full name changed to \(object.fullName)")
}

// Convenience methods for working with the change dictionary
_ = KVObserver(object: user, keyPath: \User.fullName, options: [.old, .new]) { _, change, _ in
    // unfortunately we don't know what the type of fullName is, so change uses Any
    let old = change.old as? String
    let new = change.new as? String
    if old != new {
        print("User's full name changed to \(new ?? "nil")")
    }
}

// Unregistering can be done from within the block, even in an .initial callback
_ = KVObserver(object: user, keyPath: \User.fullName, options: [.initial]) { object, _, kvo in
    guard !object.fullName.isEmpty else { return }
    print("User's full name is \(object.fullName)")
    kvo.cancel()
}

// Or you can unregister externally
let token = KVObserver(object: user, keyPath: \User.fullName) { object, _, _ in
    print("User's full name changed to \(object.fullName)")
}
// ... sometime later ...
token.cancel()

// You can also pass an observing object and KVO will be unregistered when that object deallocates
_ = KVObserver(observer: self, object: user, keyPath: \User.fullName) { observer, object, _, _ in
    // `observer` has the same type as `self`
    observer.nameLabel.text = object.fullName
}

Objective-C

Objective-C provides all the same functionality as Swift, albeit without the strong-typing of the observer/object.

// Observe an object for as long as the object is alive.
[PMKVObserver observeObject:self.user keyPath:@"fullName" options:0
                      block:^(id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nullable change, PMKVObserver * _Nonnull kvo) {
    NSLog(@"User's full name changed to %@", [object fullName]);
}];

// Change dictionary is provided, but without the convenience methods.
[PMKVObserver observeObject:self.user keyPath:@"fullName"
                    options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                      block:^(id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nullable change, PMKVObserver * _Nonnull kvo) {
    NSString *old = change[NSKeyValueChangeOldKey];
    NSString *new = change[NSKeyValueChangeNewKey];
    if (old != new && (new == nil || ![old isEqualToString:new])) {
        NSLog(@"User's full name changed to %@", new);
    }
}];

// Unregistering and observing object support is also provided (see Swift examples).

Requirements

Installing as a framework requires a minimum of iOS 8, OS X 10.9, watchOS 2.0, or tvOS 9.0.

If you install by copying the source into your project, it should work on iOS 7 or later (iOS 6 if you remove KVObserver.swift), and OS X 10.7 or later. Please note that it has not been tested on these versions.

PMKVObserver requires Xcode 9 or higher.

Installation

After installing with any mechanism, you can use this by adding import PMKVObserver (Swift) or @import PMKVObserver; (Objective-C) to your code.

Carthage

To install using Carthage, add the following to your Cartfile:

github "postmates/PMKVObserver" ~> 4.0

CocoaPods

To install using CocoaPods, add the following to your Podfile:

pod 'PMKVObserver', '~> 4.0'

Manual Installation

You may also install manually by adding the framework to your workspace, or by adding the 3 files KVObserver.h, KVObserver.m, and (optionally) KVObserver.swift to your project.

License

Licensed under either of

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be dual licensed as above, without any additional terms or conditions.

Version History

v4.1.3 (2020-04-01)

  • Work around bug with Swift compiler 5.1.5 and above relating to properties of type Any? (#25, SR-12486).

v4.1.2 (2019-01-24)

  • Fix compatibility with Swift compiler versions other than 4.2 when observing optional values and KVO delivers an NSNull value.

v4.1.1 (2018-11-28)

  • Add .object and .keyPath properties to PMKVObserver. The .keyPath property is named .objcKeyPath in Swift.

v4.1.0 (2018-11-27)

  • Handle observing optional values better. We now make stronger guarantees about when .old and .new are non-nil.

v4.0.0 (2018-09-05)

  • Set Swift version to 4.

  • Work around Xcode 10 issue with value conversions involving Any in the KVObserver.Change object (SR-8704).

  • Handle RawRepresentable types in KVObserver.Change's .old and .new properties.

    Note: Optional RawRepresentable types (such as those produced by a path like \.foo?.bar) are only supported in Swift 4.1 and later.

v3.0.2 (2018-05-22)

  • Squelch some warnings when building with CocoaPods.

v3.0.1 (2017-09-13)

  • Set Swift version back to Swift 3.2. The new KeyPath stuff is usable from 3.2 so there's no reason to require 4. PMVKObserver will still compile with Swift 3.1, but the KeyPath APIs won't be available unless compiling with Swift 3.2.

v3.0.0 (2017-09-12)

  • Convert to Swift 4.
  • Add new initializers that use Swift 4 KeyPaths.
  • Make Change.rawDict and Change.kind non-optional.

v2.0.2 (2017-07-25)

  • Switch to an unfair lock in teardown. This uses os_unfair_lock on platforms that support it, otherwise it uses an unfair mutex.

v2.0.1 (2016-09-15)

  • Fix CocoaPods.

v2.0.0 (2016-09-08)

  • Update for Swift 3.0.

v1.0.5 (2016-09-08)

  • Update for Swift 2.3.

v1.0.4 (2016-03-02)

  • Update CocoaPods podspec to split Swift support into a subspec.

v1.0.3 (2015-01-28)

  • Add property cancelled to PMKVObserver.

v1.0.2 (2016-01-26)

  • Switch to dual-licensed as MIT or Apache 2.0.

v1.0.1 (2015-12-17)

  • Stop leaking our pthread_mutex_ts.

v1.0 (2015-12-17)

Initial release.

Comments
  • Strange interaction with Realm (

    Strange interaction with Realm ("cannot remove observer…not registered")

    For Realm Objects, the dealloc spy doesn't seem to work. I think perhaps because they're trying their own variant of removing all observers when deallocating. For example, if I add a KVObserver to a Notification object's application key, when the Notification deallocs I get an exception:

    Cannot remove an observer <PMKVObserver …> for the key path "application" from <RLMAccessor_v0_Notification …> because it is not registered as an observer.

    I'm working around it for now by calling .cancel() liberally, sort of like normal KVO deregistering but at least more safe. I'm not sure if this is really a bug in PMKVObserver, but I figured I'd at least pass on my findings.

    opened by zacwest 9
  • Use Swift 3's #keyPath instead of strings

    Use Swift 3's #keyPath instead of strings

    The new #keyPath expression that landed in Swift 3 allows to designate an ObjC selector that can be enforced by the compiler: https://github.com/apple/swift-evolution/blob/master/proposals/0062-objc-keypaths.md

    It would be awesome for PMKVObserver to make use of #keyPath instead of relying on a simple String.

    opened by ldiqual 5
  • Crash in dealloc observer

    Crash in dealloc observer

    Hi @lilyball 👋

    This library has been great so far, thanks for your work on it!

    I did notice a crash in the dealloc observer when this was use for a camera session, specifically when switching between front and rear cameras.

    KVO crash

    We use a single queue for all camera operations.

    I can try and put together a repro sample project as well once I get some time to test it in isolation. For now, switching back to normal KVO seems to work fine.

    opened by iwasrobbed 4
  • Crash when observing keypath of object that changes across multiple threads

    Crash when observing keypath of object that changes across multiple threads

    The following code causes KVO to raise an exception:

    #import "ViewController.h"
    
    #import <PMKVObserver/PMKVObserver.h>
    
    @interface ChildObject : NSObject
    @end
    
    @implementation ChildObject
    @end
    
    @interface RootObject : NSObject
    @property (strong, nonatomic) ChildObject *child;
    @end
    
    @implementation RootObject
    @end
    
    @interface Observer : NSObject
    
    - (void)start;
    
    @property (atomic, nullable) RootObject *root;
    
    @end
    
    @implementation Observer
    
    - (void)start {
      [PMKVObserver observeObject:self keyPath:@"root.child" options:0
                            block:^(id object, NSDictionary<NSKeyValueChangeKey,id> *change,
                                    PMKVObserver *kvo) {}];
    
      dispatch_apply(100000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                     ^(size_t i) {
                       RootObject *root = [[RootObject alloc] init];
                       root.child = [[ChildObject alloc] init];
                       self.root = root;
                     });
    }
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
      [super viewDidLoad];
    
      Observer *observer = [[Observer alloc] init];
      [observer start];
    }
    
    
    @end
    

    I reported this issue to Apple, but they claim that this is the expected behavior.

    Given that PMKVObserver mission is to provide "Modern thread-safe and type-safe key-value observing for Swift and Objective-C", I assume that this is a use-case that the library should handle.

    opened by StatusReport 4
  • Updated pod spec not pushed to CocoaPods repo

    Updated pod spec not pushed to CocoaPods repo

    The latest pod spec with Swift and ObjC subspecs has not been pushed to the central CocoaPod repo so currently can't be used.

    Could you please update it.

    PR that was merged with the updated spec https://github.com/postmates/PMKVObserver/pull/7

    opened by bencallis 4
  • Updated podspec to includes sub spec for just ObjC

    Updated podspec to includes sub spec for just ObjC

    The existing pod spec brings in the objective-c files and swift. If using this pods in a project which includes static libs then the pod can not be used as it contains swift and there has to be a framework.

    This PR includes a 2 subspaces one for ObjC and one for Swift.

    Example usage just for ObjC

          pod 'PMKVObserver/ObjC'
    
    opened by bencallis 4
  • Swift 5.2 compiler optimization crash

    Swift 5.2 compiler optimization crash

    I've been putting off filing this issue because of procrastination and because I haven't been able to create a sample project to recreate the crash, but I'll try and provide as much info as possible.

    I work for an unnamed food delivery service that rhymes with "roast dates". We recently updated to Swift 5.2 and found a crash in production that only happened in a certain scenario with compiler optimizations on. I found that you had already discovered some strange behavior while compiler optimizations are enabled here: https://github.com/postmates/PMKVObserver/commit/778390212076933d66325756a3f91d26698e4e34

    So I updated the SDK, but still saw the same crash. The stack trace in production looks like this: Screen Shot 2020-05-26 at 4 25 45 PM

    And our code that is using the library looks like:

    extension UILabel {
        private static var map = AssocMap<(style: TextStyle, observer: KVObserver)>()
    
        var textStyle: TextStyle? {
            get {
                return UILabel.map[self]?.style
            }
    
            set {
                if let style = newValue {
                    let observer = KVObserver(object: self, keyPath: \UILabel.text, options: .new) { object, _, _ in
                        object.updateTextWithStyle()
                    }
    
                    UILabel.map[self] = (style: style, observer: observer)
                    updateTextWithStyle() // Update last, once the token/observer and style are registered
                } else {
                    UILabel.map[self]?.observer.cancel()
                    UILabel.map[self] = nil
                }
            }
        }
    

    We are using the AssocMap library as well. Do you think TSAO needs to be modified in a similar way as you did to this project?: https://github.com/lilyball/swift-tsao/blob/e15c51277b84be62cf003cd654ad02245f87e15d/Sources/TSAO.swift#L31

    Why I hesitated to file this is because I'm not sure what exactly causes the crash. The crash logs in Xcode would open unrelated places in the code where we were setting .text which makes me think it's accessing other labels in the AssocMap and finding deallocated labels. Let me know if I can provide any more info or if I should just wait until this Swift PR is merged and released and see if that resolves this issue: https://github.com/apple/swift/pull/31662

    Thank you!

    opened by jstart 3
  • Cancel all observers

    Cancel all observers

    It would be great to have a kvoController.cancelAll().

    For example, if I have a childViewController which is observing some properties only when it's on screen, then cancelling all each when disappearing, reobserving when reappearing... In the implementation, I have to keep a reference on each token, then cancel them all one by one. It's ok to do this burden, but I think that's would be a cool feature for PMKVObserver :)

    opened by tbaranes 3
  • Migrated codebase to swift 3.0 (Xcode 8 beta 2)

    Migrated codebase to swift 3.0 (Xcode 8 beta 2)

    @kballard

    Here's a fix for https://github.com/postmates/PMKVObserver/issues/10

    Most of the migration work was done automatically with the Xcode migration tool. All I had to do is to play around with NSKeyValueChangeKey and unsafeDowncast.

    All tests pass on both OSX and iOS.

    I would suggest to merge this code in a new branch swift-3.0 to keep the code separate until Xcode 8 is released.

    Let me know if you have any questions!


    This change is Reviewable

    opened by ldiqual 3
  • Investigate cost of pthread_mutex_t

    Investigate cost of pthread_mutex_t

    When replacing the spinlocks, we added a pthread_mutex_t to each observer. We need to profile this to find out how expensive it is to create and destroy these things. And if possible, find out how many system resources a mutex takes as well. If it turns out to be expensive, there's a few reasonable alternatives:

    1. Use a dispatch_queue_t instead. IIRC they're somewhere on the order of 100-200 bytes, which isn't too bad, and the system should support creating as many of these as we need.
    2. Use a bank of mutexes instead, picking one based on a hash of the observer address. This is similar to how the obj-c runtime handles its internal (safe) spinlocks. The benefit is the memory overhead is fixed no matter how many observers we create; the downside is access to a mutex can block because a different observer is using it.
    3. Use @synchronized(self). I avoided it because it's not really clear what the runtime behavior of that is; I don't know if it uses a mutex per object, or a bank of mutexes, or even something else.

    Hopefully mutexes turn out to be sufficiently cheap that we can keep the current behavior.

    opened by lilyball 3
  • Observer called on .initial but not after

    Observer called on .initial but not after

    I've been using your library for some times now and it work as intended.

    I tried today to add it to a new project but I'm unable to have the block triggered after the call for .initial. I've tried it on xcode 9, iOS11 swift 3.2 as well as Xcode 8, iOS 10 and swift 3 without success. I'm using version 2.0.2 of PMKVObserver. Even in an empty project with just the following code in the root view controller, the block is called only one time and display the initial value for my property.

    var foo:String = "foo"
    var token:KVObserver?
    
    override func viewDidLoad() {
       super.viewDidLoad()
       // Do any additional setup after loading the view, typically from a nib.
       
           token = KVObserver(object: self, keyPath: "foo", options:[NSKeyValueObservingOptions.initial, NSKeyValueObservingOptions.new]) { (object, change, kvob) in
               print(object.foo)
           }
       
           DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { [weak self] in
               guard let strongSelf = self  else {return}
               strongSelf.foo = "foooooooo"
           }
           foo = "fuuu"
    }
    

    This code result only on foo being printed one time with it's intial value : "foo". I'm pretty sure I'm missing something very obvious but cannot find what.

    opened by SeNeO 2
  • Document problems with automatic deregistration and

    Document problems with automatic deregistration and "deep" keypaths

    Experimentally, it seems that when using "deep" keypaths (keypaths with multiple components), automatic deregistration doesn't work properly. The problem is that by the time the observed object deallocates (and therefore cleans up the observer), the leaf object has already deallocated, and so it throws an exception when trying to unregister the KVO. This pattern should work if the observed object outlives self (assuming you use observer: self), or if the observed object deallocates prior to the next object in the path, but in the common case the observed object owns the deeper object and so doesn't deallocate in time.

    AFAIK the only way to truly fix this is to completely sidestep KVO's built-in handling of "deep" keypaths and rebuild that ourselves, so that way we can attach the dealloc spy to every component of the path. This is a lot of work and would be very tricky to get right so I'm reluctant to do this. Alternatively we could actually isa-swizzle the observed object in order to clean up KVO at the start of dealloc instead of the end, but that's really nasty.

    In the meantime, we should document this scenario.

    opened by lilyball 4
Releases(v4.1.3)
Owner
Postmates Inc.
Postmates Inc.
Type-safe event handling for Swift

emitter-kit v5.2.2 A replacement for NSNotificationCenter#addObserver and NSObject#addObserver that is type-safe and not verbose. import EmitterKit /

Alec Larson 570 Nov 25, 2022
NoticeObserveKit is type-safe NotificationCenter wrapper.

NoticeObserveKit NoticeObserveKit is type-safe NotificationCenter wrapper. // .keyboardWillShow is a static property. Notice.Center.default.observe(na

Taiki Suzuki 147 Nov 24, 2022
TopicEventBus is Easy to use, type safe way of implementing Publish–subscribe design pattern.

TopicEventBus Publish–subscribe design pattern implementation framework, with ability to publish events by topic. (NotificationCenter extended alterna

Matan Abravanel 55 Nov 29, 2021
A New, Modern Reactive State Management Library for Swift and SwiftUI (The iOS implementation of Recoil)

RecoilSwift RecoilSwift is a lightweight & reactive swift state management library. RecoilSwift is a SwiftUI implementation of recoil.js which powered

Holly Li 160 Dec 25, 2022
Swift Apps in a Swoosh! A modern framework for creating iOS apps, inspired by Redux.

Katana is a modern Swift framework for writing iOS applications' business logic that are testable and easy to reason about. Katana is strongly inspire

Bending Spoons 2.2k Jan 1, 2023
Write great asynchronous code in Swift using futures and promises

BrightFutures How do you leverage the power of Swift to write great asynchronous code? BrightFutures is our answer. BrightFutures implements proven fu

Thomas Visser 1.9k Dec 20, 2022
A Swift based Future/Promises Library for IOS and OS X.

FutureKit for Swift A Swift based Future/Promises Library for IOS and OS X. Note - The latest FutureKit is works 3.0 For Swift 2.x compatibility use v

null 759 Dec 2, 2022
A library for reactive and unidirectional Swift applications

ReactorKit is a framework for a reactive and unidirectional Swift application architecture. This repository introduces the basic concept of ReactorKit

ReactorKit 2.5k Dec 28, 2022
VueFlux is the architecture to manage state with unidirectional data flow for Swift, inspired by Vuex and Flux.

Unidirectional State Management Architecture for Swift - Inspired by Vuex and Flux Introduction VueFlux is the architecture to manage state with unidi

Ryo Aoyama 324 Dec 17, 2022
Bond is a Swift binding framework that takes binding concepts to a whole new level.

Bond, Swift Bond Update: Bond 7 has been released! Check out the migration guide to learn more about the update. Bond is a Swift binding framework tha

Declarative Hub 4.2k Jan 5, 2023
Easy Swift Futures & Promises.

❗️ Archived now ❗️ Since Apple released Combine framework, I decide to archive this repo. You still can use this repo as an example of Future/Promise

Dmytro Mishchenko 40 Sep 23, 2022
📡 Helping you own NotificationCenter in Swift!

Notificationz ?? Helping you own NotificationCenter Highlights Keep Your Naming Conventions: This library gives you convenient access to NotificationC

Kitz 77 Feb 18, 2022
Observable is the easiest way to observe values in Swift.

Observable is the easiest way to observe values in Swift. How to Create an Observable and MutableObservable Using MutableObservable you can create and

Robert-Hein Hooijmans 368 Nov 9, 2022
ReSwift is a Redux-like implementation of the unidirectional data flow architecture in Swift.

ReSwift is a Redux-like implementation of the unidirectional data flow architecture in Swift. ReSwift helps you to separate three important concerns of your app's components.

null 7.3k Jan 9, 2023
🐌 snail - An observables framework for Swift

?? snail A lightweight observables framework, also available in Kotlin Installation Carthage You can install Carthage with Homebrew using the followin

Compass 179 Nov 21, 2022
Lightweight Promises for Swift & Obj-C

Tomorrowland Tomorrowland is an implementation of Promises for Swift and Objective-C. A Promise is a wrapper around an asynchronous task that provides

Lily Ballard 115 Nov 23, 2022
When is a lightweight implementation of Promises in Swift

Description When is a lightweight implementation of Promises in Swift. It doesn't include any helper functions for iOS and OSX and it's intentional, t

Vadym Markov 259 Dec 29, 2022
EventBroadcaster is a lightweight event handler framework, written in swift for iOS, macOS, tvOS & watchOS applications.

EventBroadcaster is a lightweight event handler framework, written in swift for iOS, macOS, tvOS & watchOS applications.

Ali Samaiee 4 Oct 5, 2022
Define and chain Closures with Inputs and Outputs

Closure Define and chain Closures with Inputs and Outputs Examples No Scoped State let noStateCount = Closure<String, String> { text in String(repea

Zach Eriksen 3 May 18, 2022