Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Noble Cocoa

Noble Cocoa

An explanation of a project I put together that describes how it's possible to minimize state by using KVO and dependent keys.

Noble Cocoa is an exploration and demonstration of how some of the ideas presented by the ReactiveCocoa can be applied to a Cocoa app that just uses KVO.

Based on my GitHub repo: https://github.com/mdiep/NobleCocoa

Matt Diephouse

January 08, 2013
Tweet

More Decks by Matt Diephouse

Other Decks in Programming

Transcript

  1. Matt Diephouse @mdiep CocoaHeads January 8, 2013 Noble Cocoa Minimizing

    State with KVO After seeing Josh Abernathy’s presentation on ReactiveCocoa, I started reading up on some of the ideas behind it and looking at its documentation. I thought some of the ideas could be applicable to Cocoa applications that don’t use ReactiveCocoa, so I put this together to play with and demonstrate that.
  2. Variables† in a program that change over time †includes properties

    that are backed by variables This does NOT include local variables that are redefined within a method or function. The distinguishing feature of functional programming languages is that they lack mutable state. This means that they are less dependent on time (referential transparency).
  3. State → Complexity? •State is hard to test •State must

    be reasoned about •State depends on order •State requires more code
  4. Minimize state Derive what you can If a variable is

    just a combination of other variables, then don’t store it: recompute it when needed. This is a premise of ReactiveCocoa.
  5. “There are only two hard things in Computer Science: cache

    invalidation and naming things.” —Phil Karlton State bugs are another form of cache invalidation bugs.
  6. For more details, read “Out of the Tar Pit” Full

    disclosure: I have not finished reading this paper.
  7. Observe - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

    { if ([keyPath isEqual:@“property”]) { NSLog(@“property changed!”); } else [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }
  8. Use Contexts static void * __MyClassPropertyChangedContext = @“__MyClassPropertyChangedContext”; - (void)

    awakeFromNib { [object addObserver:self forKeyPath:@“property” options:0 context:__MyClassPropertyChangedContext]; } - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == __MyClassPropertyChangedContext) NSLog(@“property changed!”); else ...; } Don’t stomp on a superclass’s notifications
  9. Configure in Setters - (void) dealloc { self.object = nil;

    } - (void) setObject:(id)anObject { if (anObject != self.object) { if (self.object) [self.object removeObserver:self forKeyPath:@“property”]; _object = anObject; if (self.object) [self.object addObserver:self forKeyPath:@“property” options:0 context:__MyClassPropertyChangedContext]; } } • Ensure that add/remove observer is balanced • Consolidate keypaths, options, and contexts in a single method
  10. Know the current value and know when it changes These

    are the 2 things you need to know in order to replace mutable state.
  11. The example app. Reset all the fields to be empty

    as soon as any of them has a value. Create when all the fields have a value and the emails match.
  12. @interface NCAppController : NSObject <NSApplicationDelegate> @property IBOutlet NSButton *resetButton; @property

    IBOutlet NSButton *createButton; @property IBOutlet NSTextField *firstNameField; @property (assign, readonly) BOOL canReset; @property (assign, readonly) BOOL canCreate; - (IBAction) firstNameChanged:(id)sender; - (IBAction) resetButtonClicked:(id)sender; @end Stateful Interface The example controller, with some of the outlets defined. We’ll ignore all the fields except for the firstName for the sake of brevity. There are 2 readonly properties that will return whether each button will be enabled. -canReset returns YES if any text field has a non-empty value -canCreate returns YES if all text fields are non-empty and the 2 emails match
  13. Stateful Implementation @implementation NCAppController - (IBAction) firstNameChanged:(id)sender { self.resetButton.enabled =

    self.canReset; self.createButton.enabled = self.canCreate; } - (IBAction) resetButtonClicked:(id)sender { self.firstNameField.stringValue = nil; self.resetButton.enabled = NO; } @end This code is buggy: the reset method doesn’t disable the create button! Any method that changes a value is responsible for setting the enabled property for both buttons. This is a very basic app. But it’s not hard to imagine making a mistake like this.
  14. @interface NCAppController : NSObject <NSApplicationDelegate> @property IBOutlet NSButton *resetButton; @property

    IBOutlet NSButton *createButton; @property IBOutlet NSTextField *firstNameField; @property (copy) NSString *firstName; @property (copy) NSString *lastName; @property (copy) NSString *email; @property (copy) NSString *reEmail; @property (assign, readonly) BOOL canReset; @property (assign, readonly) BOOL canCreate; - (IBAction) firstNameChanged:(id)sender; - (IBAction) resetButtonClicked:(id)sender; @end Noble Interface Here’s what I’m calling the “noble” version. The interface adds 4 properties to store the values of the different fields. You could argue that this is added state. But it’s really a workaround for the fact that NSTextField doesn’t isn’t KVO-compliant for its value. We’re going to assume that KVO is used to keep the text fields in sync with the property. When the property changes, the text field’s value is changed. This has a minimal risk for bugs because they only need to be updated in one spot. On the Mac, you can use bindings for this; I did in my repo on GitHub. We know how to get the current value of -canReset. But how do we know when it changes?
  15. Dependent Keys + (NSSet *) keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths

    = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@“fullName”]) { NSArray *affectingKeys = @[@“lastName”, @“firstName”]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } + (NSSet *) keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@“lastName”, @“firstName”, nil]; }
  16. Noble Implementation @implementation NCAppController + (NSSet *) keyPathsForValuesAffectingCanReset { return

    [NSSet setWithObjects:@“firstName”, @“lastName”, @“email”, @“reEmail”, nil]; } + (NSSet *) keyPathsForValuesAffectingCanCreate { return [NSSet setWithObjects:@“firstName”, @“lastName”, @“email”, @“reEmail”, nil]; } - (IBAction) resetButtonClicked:(id)sender { self.firstName = nil; } @end Registering the canReset and canCreate keys as dependent on those keyPaths means that when one of them changes, a KVO notification will be sent. Observing this value (or binding to it) means that the button’s enabled state will get updated correctly. This state—the enabled state for the buttons—is now derived. We’ve minimized the amount of state to almost zero. This removes the potential for bugs.
  17. Q&A