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 deinit
s 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 nil
ing 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
andValue2
need to be the same for assignmentT
needs to represent the type ofself
- KVC/KVO target properties need to be
@objc
BindMe.init(person:)
needssuper.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 ofself
(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
How to Sort JSON Coming from Alamofire and Return Final JSON Object (Swiftyjson)
Deleting with One-To-Many Relationship
Swift Notification Fire from Datepicker
Content Blocker Extension with a String Instead of a File
Swift:How to Handle a Lot of Textures in Memory
Create Generic Delegate for Class
Nstimer Not Stopping When Invalidated in This Function
Avspeechsynthesizer Errors in iOS 10
Reflection with Swift - Get Functions Name of a Class
Saving Exif Data to Jpeg - Swift
Swift Probability of Random Number Being Selected
Swift Threading with Dispatchgroup
Xcode Swift. How to Programmatically Select Cell in View-Based Nstableview
Uicollectionview Scrolltoitematindexpath, Not Loading Visible Cells Until Animation Complete