How to Use Key-Value Observing with Smart Keypaths in Swift 4

How can I use Key-Value Observing with Smart KeyPaths in Swift 4?

As for your initial code, here's what it should look like:

class myArrayController: NSArrayController {
private var mySub: Any? = nil

required init?(coder: NSCoder) {
super.init(coder: coder)

self.mySub = self.observe(\.content, options: [.new]) { object, change in
debugPrint("Observed a change to", object.content)
}
}
}

The observe(...) function returns a transient observer whose lifetime indicates how long you'll receive notifications for. If the returned observer is deinit'd, you will no longer receive notifications. In your case, you never retained the object so it died right after the method scope.

In addition, to manually stop observing, just set mySub to nil, which implicitly deinits the old observer object.

Safe Key Value Observing of keypaths

Edit 2

After spending some more time with KVO, I found that in your case, you should be observing person.workplace.address instead of workplace.address. When you observe person.workplace.address, you achieve 2 things:

1) Since you owned the person object, you have absolute control over your object's life cycle. You can now removeObserver at a time of your own choosing.

2) When workplace object is changed, the KVO mechanism will "automagically" observe the new address of the new workplace. Of course, it will notify you of the new address.

Now, you can observe the address without fearing workplace being replaced at random time. This is one of the true hidden power of KVO. This allow subclasses to safely observe any superclass's objects without knowing about their life-cycles.

Edit 1

Best practice to remove an object as observer for some KVO property

The accepted answer in this thread best described your situation. You should not have observe the property address in the first place, since you have no control over the life-cycle of workplace. You have a design issue.

Original

You can observe the keyPath workplace on the person object. When this keyPath is invoked, you simply removeObserver for workplace.address.

[person addObserver:theObserver
forKeyPath:@"workplace"
options:[NSKeyValueObservingOptionNew]
context:NULL];

Swift 4 approach for observeValue(forKeyPath:...)

Swift 4 introduced a family of concrete Key-Path types, a new Key-Path Expression to produce them and a new closure-based observe function available to classes that inherit NSObject.

Using this new set of features, your particular example can now be expressed much more succinctly:

self.observation = object.observe(\.keyPath) { 
[unowned self] object, change in
self.someFunction()
}

Types Involved

  • observation:NSKeyValueObservation
  • change:NSKeyValueObservedChange
  • \.keyPath: An instance of a KeyPath class produced at compile time.

Key-Path grammar

The general grammar of a Key-Path Expression follows the form \Type.keyPath where Type is a concrete type name (incl. any generic parameters), and keyPath a chain of one or more properties, subscripts, or optional chaining/forced unwrapping postfixes. In addition, if the keyPath's Type can be inferred from context, it can be elided, resulting in a most pithy \.keyPath.

These are all valid Key-Path Expressions:

\SomeStruct.someValue
\.someClassProperty
\.someInstance.someInnerProperty
\[Int].[1]
\[String].first?.count
\[SomeHashable: [Int]].["aStringLiteral, literally"]!.count.bitWidth

Ownership

You're the owner of the NSKeyValueObservation instance the observe function returns, meaning, you don't have to addObserver nor removeObserver anymore; rather, you keep a strong reference to it for as long as you need your observation observing.

You're not required to invalidate() either: it'll deinit gracefully. So, you can let it live until the instance holding it dies, stop it manually by niling the reference, or even invoke invalidate() if you need to keep your instance alive for some smelly reason.

Caveats

As you may have noticed, observation still lurks inside the confines of Cocoa's KVO mechanism, therefore it's only available to Obj-C classes and Swift classes inheriting NSObject (every Swift-dev's favorite type) with the added requirement that any value you intend to observe, must be marked as @objc (every Swift-dev's favorite attribute) and declared dynamic.

That being said, the overall mechanism is a welcomed improvement, particularly because it manages to Swiftify observing imported NSObjects from modules we may happen to be required to use (eg. Foundation), and without risking weakening the expressive power we work so hard to obtain with every keystroke.

As a side-note, Key-Path String Expressions are still required to dynamically access NSObject's properties to KVC or call value(forKey(Path):)

Beyond KVO

There's much more to Key-Path Expressions than KVO. \Type.path expressions can be stored as KeyPath objects for later reuse. They come in writable, partial and type-erased flavors. They can augment the expressive power of getter/setter functions designed for composition, not to mention the role they play in allowing those with the strongest of stomachs to delve into the world of functional concepts like Lenses and Prisms. I suggest you check the links down below to learn more about the many development doors they can open.

Links:

Key-Path Expression @ docs.swift.org

KVO docs @ Apple

Swift Evolution Smart KeyPaths proposal

Ole Begemann's Whats-new-in-Swift-4 playground with Key-Path examples

WWDC 2017 Video: What's New in Foundation 4:35 for SKP and 19:40 for KVO.

Using NSKeyValueObservation to observe value in UserDefaults

Yes its possible.First of all you need to define keypath as

extension UserDefaults
{
@objc dynamic var isRunningWWDC: Bool
{
get {
return bool(forKey: "isRunningWWDC")
}
set {
set(newValue, forKey: "isRunningWWDC")
}
}
}

And use that keypath for block based KVO as

var observerToken:NSKeyValueObservation?
observerToken = UserDefaults.standard.observe(\.isRunningWWDC, options:[.new,.old])
{ (object, change) in

print("Change is \(object.isRunningWWDC)")

}
UserDefaults.standard.isRunningWWDC = true

Can the non-string property name passed to #keyPath() be saved independently?

It doesn't look like it is possible.


Here's the compiler's code to parse a key path expression:

///   expr-keypath:
/// '#keyPath' '(' unqualified-name ('.' unqualified-name) * ')'
///
ParserResult<Expr> Parser::parseExprKeyPath() {
// Consume '#keyPath'.
SourceLoc keywordLoc = consumeToken(tok::pound_keyPath);

// Parse the leading '('.
if (!Tok.is(tok::l_paren)) {
diagnose(Tok, diag::expr_keypath_expected_lparen);
return makeParserError();
}
SourceLoc lParenLoc = consumeToken(tok::l_paren);

// Handle code completion.
SmallVector<Identifier, 4> names;
SmallVector<SourceLoc, 4> nameLocs;
auto handleCodeCompletion = [&](bool hasDot) -> ParserResult<Expr> {
ObjCKeyPathExpr *expr = nullptr;
if (!names.empty()) {
expr = ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
nameLocs, Tok.getLoc());
}

if (CodeCompletion)
CodeCompletion->completeExprKeyPath(expr, hasDot);

// Eat the code completion token because we handled it.
consumeToken(tok::code_complete);
return makeParserCodeCompletionResult(expr);
};

// Parse the sequence of unqualified-names.
ParserStatus status;
while (true) {
// Handle code completion.
if (Tok.is(tok::code_complete))
return handleCodeCompletion(!names.empty());

// Parse the next name.
DeclNameLoc nameLoc;
bool afterDot = !names.empty();
auto name = parseUnqualifiedDeclName(
afterDot, nameLoc,
diag::expr_keypath_expected_property_or_type);
if (!name) {
status.setIsParseError();
break;
}

// Cannot use compound names here.
if (name.isCompoundName()) {
diagnose(nameLoc.getBaseNameLoc(), diag::expr_keypath_compound_name,
name)
.fixItReplace(nameLoc.getSourceRange(), name.getBaseName().str());
}

// Record the name we parsed.
names.push_back(name.getBaseName());
nameLocs.push_back(nameLoc.getBaseNameLoc());

// Handle code completion.
if (Tok.is(tok::code_complete))
return handleCodeCompletion(false);

// Parse the next period to continue the path.
if (consumeIf(tok::period))
continue;

break;
}

// Parse the closing ')'.
SourceLoc rParenLoc;
if (status.isError()) {
skipUntilDeclStmtRBrace(tok::r_paren);
if (Tok.is(tok::r_paren))
rParenLoc = consumeToken();
else
rParenLoc = PreviousLoc;
} else {
parseMatchingToken(tok::r_paren, rParenLoc,
diag::expr_keypath_expected_rparen, lParenLoc);
}

// If we cannot build a useful expression, just return an error
// expression.
if (names.empty() || status.isError()) {
return makeParserResult<Expr>(
new (Context) ErrorExpr(SourceRange(keywordLoc, rParenLoc)));
}

// We're done: create the key-path expression.
return makeParserResult<Expr>(
ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
nameLocs, rParenLoc));
}

This code first creates a list of period-separated names inside the parentheses, and then it attempts to parse them as an expression. It accepts an expression and not data of any Swift type; it accepts code, not data.

Binding 2 properties (observe) using keyPath

According to the accepted proposal SE-0161 Smart KeyPaths: Better Key-Value Coding for Swift, you need to use ReferenceWritableKeyPath to write a value to the key path for an object with reference semantics, using subscript.

(You need to pass a classic String-based key path to setValue(_:forKeyPath:), not KeyPath.)

And some more:

  • Value and Value2 need to be the same for assignment
  • T needs to represent the type of self
  • KVC/KVO target properties need to be @objc
  • BindMe.init(person:) needs super.init()

So, your BindMe would be something like this:

class BindMe: NSObject {
var observers = [NSKeyValueObservation]()
@objc let person: Person

@objc var myFirstName: String = "<no first name>"
@objc var myLastName: String = "<no last name>"

init(person: Person) {
self.person = person
super.init()
self.setupBindings()
}

func setupBindings() {
self.bind(to: \BindMe.myFirstName, from: \BindMe.person.firstName)
self.bind(to: \BindMe.myLastName, from: \BindMe.person.lastName)
}

func bind<Value>(to targetKeyPath: ReferenceWritableKeyPath<BindMe, Value>, from sourceKeyPath: KeyPath<BindMe, Value>) {
self.observers.append(self.observe(sourceKeyPath, options: [.initial, .new], changeHandler: { (object, change) in
self[keyPath: targetKeyPath] = change.newValue!
}))
}
}

For EDIT:

The demand to make a BindBase like thing seems very reasonable, so I have made some tries.

To fulfill

  • T needs to represent the type of self

(where T == KeyPath.Root), using Self would be the most instinctive, but unfortunately, its usage is still very restricted in the current version of Swift.

You can move the definition of bind into a protocol extension using Self:

class BindBase: NSObject, Bindable {
var observers = [NSKeyValueObservation]()
}

protocol Bindable: class {
var observers: [NSKeyValueObservation] {get set}
}

extension Bindable {
func bind<Value>(to targetKeyPath: ReferenceWritableKeyPath<Self, Value>, from sourceKeyPath: KeyPath<Self, Value>)
where Self: NSObject
{
let observer = self.observe(sourceKeyPath, options: [.initial, .new]) {object, change in
self[keyPath: targetKeyPath] = change.newValue!
}
self.observers.append(observer)
}
}

Key-Value Observing NSCache

I would expect NSCache not to be key-value observing (KVO) compliant for that sort of key. KVO is implemented internally at the NSObject level by replacing the normal setter with one that alerts relevant observers and calls the original setter. In the case of things you set with setObject:forKey: there is no specific setter so nothing for the runtime to hang off.

Since NSCache doesn't post any relevant notifications I think your best hope is the delegate protocol. It's not explicit exactly what counts as an eviction but if cache:willEvictObject: is called when the object associated with a key is changed on purpose then you could hook directly off that.

Otherwise I'd recommend you create a wrapper class for NSCache, say DWCache for argument's sake, that contains an NSCache, is the delegate of the cache, and provides its own setObject:forKey:. It will posts an appropriate message (i) upon the first call to setObject:forKey:; (ii) upon every subsequent call that supplies an object different from that already in the cache; and (iii) whenever it receives a cache:willEvictObject:.

The only potential complexity is that NSCache doesn't copy the original keys and, partly as a result, has no means to get key from object. You may want to store those connections separately, e.g. through an NSDictionary. Provided you clear the dictionary when the cache evicts the object you'll still be providing caching behaviour.

How one model can notify another model for specific property has changed?

Thanks for El Tomato comment, delegation can be a pattern be used.

A delegation pattern would work since in my case Model A is only used by a single entity Model B.

protocol ModelADelegate {
func fooDidChange() -> Void
}

class ModelA {
public var delegate: ModelADelegate?

private(set) var foo: CustomObjectClassName {
didSet {
delegate.fooDidChange()
}
}

static let shared = ModelA()
}

class ModelB, ModelADelegate {
private var bar: ModelC

init() {
ModelA.shared.delegate = self

self.bar = ModelC(ModelA.shared.foo)
}

func fooDidChange() {
self.bar = ModelC(ModelA.shared.foo)
}
}

Can I observe an optional value in swift? If not, how might I go about attempting to observe a change?

Many properties of many AppKit objects are not KVO-compliant. Unless the documentation specifically says the property is compliant, you should assume it's not compliant. NSPopUpButton's selectedItem property is non-compliant.

The easiest way to be notified that the pop-up button's selected item changed is to set the button's target and action:

    override func viewDidLoad() {
super.viewDidLoad()
myPopUpButton.target = self
myPopUpButton.action = #selector(popUpButtonDidFire(_:))
}

@IBAction private func popUpButtonDidFire(_ sender: Any) {
// code to execute goes here
}

Note that if you're creating the pop-up button in a storyboard or xib, you can wire it to the popUpButtonDidFire method by control-dragging from the pop-up button to the view controller.



Related Topics



Leave a reply



Submit