Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better modeling for KVO collection changes #1032

Merged
merged 28 commits into from
Jan 24, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e5d6b03
Create collection mutation objects for reifying to-many changes
jspahrsummers Dec 26, 2013
2e93ed2
Add initializers to all mutation classes
jspahrsummers Dec 26, 2013
21a6d22
Add KVO method for receiving RACCollectionMutations
jspahrsummers Dec 26, 2013
7f192cb
Add -map to <RACCollectionMutation>
jspahrsummers Dec 26, 2013
cfe938b
Add an example to -rac_valuesAndCollectionMutations…
jspahrsummers Dec 26, 2013
4536c42
Merge branch '3.0-development' into mutations
jspahrsummers Dec 30, 2013
6b0eba3
First pass at NSTableView bindings
jspahrsummers Jan 2, 2014
127ed6f
Clarify that values aren't used for NSTableView mutation
jspahrsummers Jan 2, 2014
77100a8
Move `indexes` out of the protocol
jspahrsummers Jan 3, 2014
dd5f95b
Add new headers to ReactiveCocoa.h
jspahrsummers Jan 3, 2014
cb5051f
Add support for moving objects in a collection
jspahrsummers Jan 3, 2014
f5ea914
Added bindings for individual UITableView sections
jspahrsummers Jan 3, 2014
d7cfc81
Document all the things
jspahrsummers Jan 3, 2014
ce1f42e
Merge branch '3.0-development' into mutations
jspahrsummers Jan 23, 2014
2c52b4c
Add UICollectionView mutation bindings
jspahrsummers Jan 23, 2014
26911ec
Merge remote-tracking branch 'origin/3.0-development' into mutations
jspahrsummers Jan 23, 2014
f141125
Tests for unordered mutations
jspahrsummers Jan 24, 2014
c5cb0c6
Tests for ordered mutations
jspahrsummers Jan 24, 2014
fc0fcea
Mutations aren't sent for keys without to-many accessors
jspahrsummers Jan 24, 2014
408f4e2
Remove protocol assertions, the value may be immutable
jspahrsummers Jan 24, 2014
4a0ca3a
Implement <NSCopying> and <NSObject> for mutations
jspahrsummers Jan 24, 2014
3574c85
Require <RACCollection>s to support fast enumeration
jspahrsummers Jan 24, 2014
d6857bf
Use mutable collections on RACTestObject
jspahrsummers Jan 24, 2014
7ffbcdb
Combine describe() blocks for -rac_valuesAndChanges…
jspahrsummers Jan 24, 2014
fceaa56
Convert all KVO change collections to arrays
jspahrsummers Jan 24, 2014
eff69df
Tests for -rac_valuesAndCollectionMutations…
jspahrsummers Jan 24, 2014
70384d5
Treat unordered replacements as RACSettingMutations
jspahrsummers Jan 24, 2014
e220be7
Merge remote-tracking branch 'origin/3.0-development' into mutations
jspahrsummers Jan 24, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
202 changes: 202 additions & 0 deletions ReactiveCocoaFramework/ReactiveCocoa.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions ReactiveCocoaFramework/ReactiveCocoa/NSArray+RACSupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import <Foundation/Foundation.h>
#import "RACDeprecated.h"
#import "RACOrderedCollection.h"

@class RACSequence;
@class RACSignal;
Expand All @@ -21,6 +22,9 @@

@end

@interface NSMutableArray (RACCollectionSupport) <RACOrderedCollection>
@end

@interface NSArray (RACSupportDeprecated)

@property (nonatomic, copy, readonly) RACSequence *rac_sequence RACDeprecated("Use -rac_signal instead");
Expand Down
34 changes: 34 additions & 0 deletions ReactiveCocoaFramework/ReactiveCocoa/NSArray+RACSupport.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,40 @@ - (RACSignal *)rac_signal {

@end

@implementation NSMutableArray (RACCollectionSupport)

- (void)rac_addObjects:(NSArray *)objects {
[self addObjectsFromArray:objects];
}

- (void)rac_removeObjects:(NSArray *)objects {
[self removeObjectsInArray:objects];
}

- (void)rac_replaceAllObjects:(NSArray *)objects {
[self setArray:objects];
}

- (void)rac_insertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexSet {
[self insertObjects:objects atIndexes:indexSet];
}

- (void)rac_removeObjectsAtIndexes:(NSIndexSet *)indexSet {
[self removeObjectsAtIndexes:indexSet];
}

- (void)rac_replaceObjectsAtIndexes:(NSIndexSet *)indexSet withObjects:(NSArray *)objects {
[self replaceObjectsAtIndexes:indexSet withObjects:objects];
}

- (void)rac_moveObjectAtIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex {
id object = self[fromIndex];
[self removeObjectAtIndex:fromIndex];
[self insertObject:object atIndex:toIndex];
}

@end

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import <Foundation/Foundation.h>
#import "RACDeprecated.h"
#import "RACCollection.h"

@class RACSequence;
@class RACSignal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,54 @@

/// Creates a signal to observe the value at the given key path.
///
/// The initial value is sent on subscription, the subsequent values are sent
/// from whichever thread the change occured on, even if it doesn't have a valid
/// The initial value is sent on subscription. Subsequent values are sent from
/// whichever thread the change occured on, even if it doesn't have a valid
/// scheduler.
///
/// Returns a signal that immediately sends the receiver's current value at the
/// given keypath, then any changes thereafter.
- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath;

/// Creates a signal to observe the changes of the given key path.
/// Creates a signal to observe the changes to the <RACCollection> at the given
/// key path.
///
/// Note that granular changes (like insertion and deletion) will only be sent
/// if changes to the given key path are made through KVC's to-many accessor
/// methods. See:
/// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/AccessorConventions.html
///
/// The initial value is sent on subscription, the subsequent values are sent
/// from whichever thread the change occured on, even if it doesn't have a valid
/// The initial value is sent on subscription. Subsequent values are sent from
/// whichever thread the change occured on, even if it doesn't have a valid
/// scheduler.
///
/// Examples
///
/// [[[self
/// rac_valuesAndCollectionMutationsForKeyPath:@keypath(self.models)]
/// reduceEach:^(id _, id<RACOrderedCollectionMutation> modelsMutation) {
/// return [modelsMutation map:^(Model *model) {
/// return [[ViewModel alloc] initWithModel:model];
/// }];
/// }]
/// subscribeNext:^(id<RACOrderedCollectionMutation> viewModelsMutation) {
/// @strongify(self);
///
/// NSMutableArray *VMs = [self mutableArrayValueForKey:@keypath(self.viewModels)];
/// [viewModelsMutation mutateCollection:VMs];
/// }];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern reminds me of RAC(…) = RACObserve(…). What are your thoughts on similar macros?

(macro names for illustration)

RACMutate(self, viewModels) = [RACObserveMutations(self, models)
    reduceEach:^(id _, id<RACOrderedCollectionMutation> modelsMutation) {
        return [modelsMutation map:^(Model *model) {
            return [[ViewModel alloc] initWithModel:model];
        }];
    }];

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collection updates are comparatively uncommon, and we already get flak for the common macros. I'd rather not bloat the framework further with more.

///
/// Returns a signal that sends tuples containing the current collection at the
/// key path and a <RACCollectionMutation> describing the change that occurred.
/// If the collection is specifically a <RACOrderedCollection>, the collection
/// mutation will conform to <RACOrderedCollectionMutation>.
- (RACSignal *)rac_valuesAndCollectionMutationsForKeyPath:(NSString *)keyPath;

/// Creates a signal to observe the changes of the given key path.
///
/// The initial value (if `NSKeyValueObservingOptionInitial` is specified) is
/// sent on subscription. Subsequent values are sent from whichever thread the
/// change occured on, even if it doesn't have a valid scheduler.
///
/// Returns a signal that sends tuples containing the current value at the key
/// path and the change dictionary for each KVO callback.
- (RACSignal *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,42 @@
//

#import "NSObject+RACPropertySubscribing.h"

#import "EXTScope.h"
#import "NSObject+RACDeallocating.h"
#import "NSObject+RACDescription.h"
#import "NSObject+RACKVOWrapper.h"
#import "RACCompoundDisposable.h"
#import "RACDisposable.h"
#import "RACInsertionMutation.h"
#import "RACKVOTrampoline.h"
#import "RACSubscriber.h"
#import "RACMinusMutation.h"
#import "RACRemovalMutation.h"
#import "RACReplacementMutation.h"
#import "RACSettingMutation.h"
#import "RACSignal+Operations.h"
#import "RACSubscriber.h"
#import "RACTuple.h"
#import "RACUnionMutation.h"

#import <libkern/OSAtomic.h>

static NSArray *RACConvertToArray(id collection) {
if (collection == nil) return @[];
if ([collection isKindOfClass:NSArray.class]) return collection;
if ([collection isKindOfClass:NSSet.class]) return [collection allObjects];
if ([collection isKindOfClass:NSOrderedSet.class]) return [collection array];

NSCParameterAssert([collection conformsToProtocol:@protocol(NSFastEnumeration)]);

NSMutableArray *enumeratedObjects = [[NSMutableArray alloc] init];
for (id obj in collection) {
[enumeratedObjects addObject:obj];
}

return enumeratedObjects;
}

@implementation NSObject (RACPropertySubscribing)

- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath {
Expand All @@ -30,6 +54,73 @@ - (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath {
setNameWithFormat:@"RACObserve(%@, %@)", self.rac_description, keyPath];
}

- (RACSignal *)rac_valuesAndCollectionMutationsForKeyPath:(NSString *)keyPath {
return [[[self
rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial]
reduceEach:^(id value, NSDictionary *change) {
NSCAssert(value == nil || [value conformsToProtocol:@protocol(NSFastEnumeration)], @"Expected an enumerable collection at key path \"%@\", instead got %@", keyPath, value);

NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue];
NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
id oldObjects = change[NSKeyValueChangeOldKey];
id newObjects = change[NSKeyValueChangeNewKey];

NSObject<RACCollectionMutation> *mutation;

switch (kind) {
case NSKeyValueChangeReplacement:
if (indexes != nil) {
// Only use `RACReplacementMutation` for ordered
// collections.
oldObjects = RACConvertToArray(oldObjects);
newObjects = RACConvertToArray(newObjects);

mutation = [[RACReplacementMutation alloc] initWithRemovedObjects:oldObjects addedObjects:newObjects indexes:indexes];
break;
}

// Otherwise, fall through and act like the entire
// collection was replaced (see `NSKeyValueSetSetMutation`).
newObjects = value;

case NSKeyValueChangeSetting:
newObjects = RACConvertToArray(newObjects);
mutation = [[RACSettingMutation alloc] initWithObjects:newObjects];

break;

case NSKeyValueChangeInsertion:
newObjects = RACConvertToArray(newObjects);

if (indexes == nil) {
mutation = [[RACUnionMutation alloc] initWithObjects:newObjects];
} else {
mutation = [[RACInsertionMutation alloc] initWithObjects:newObjects indexes:indexes];
}

break;

case NSKeyValueChangeRemoval:
oldObjects = RACConvertToArray(oldObjects);

if (indexes == nil) {
mutation = [[RACMinusMutation alloc] initWithObjects:oldObjects];
} else {
mutation = [[RACRemovalMutation alloc] initWithObjects:oldObjects indexes:indexes];
}

break;

default:
NSCAssert(NO, @"Unrecognized KVO change kind: %lu", (unsigned long)kind);
__builtin_unreachable();
}

return RACTuplePack(value, mutation);
}]
setNameWithFormat:@"%@ -rac_valuesAndCollectionMutationsForKeyPath: %@", self.rac_description, keyPath];
}

- (RACSignal *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options {
keyPath = [keyPath copy];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import <Foundation/Foundation.h>
#import "RACDeprecated.h"
#import "RACOrderedCollection.h"

@class RACSequence;
@class RACSignal;
Expand All @@ -21,6 +22,9 @@

@end

@interface NSMutableOrderedSet (RACCollectionSupport) <RACOrderedCollection>
@end

@interface NSOrderedSet (RACSupportDeprecated)

@property (nonatomic, copy, readonly) RACSequence *rac_sequence RACDeprecated("Use -rac_signal instead");
Expand Down
33 changes: 33 additions & 0 deletions ReactiveCocoaFramework/ReactiveCocoa/NSOrderedSet+RACSupport.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,39 @@ - (RACSignal *)rac_signal {

@end

@implementation NSMutableOrderedSet (RACCollectionSupport)

- (void)rac_addObjects:(NSArray *)objects {
[self addObjectsFromArray:objects];
}

- (void)rac_removeObjects:(NSArray *)objects {
[self removeObjectsInArray:objects];
}

- (void)rac_replaceAllObjects:(NSArray *)objects {
[self removeAllObjects];
[self addObjectsFromArray:objects];
}

- (void)rac_insertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexSet {
[self insertObjects:objects atIndexes:indexSet];
}

- (void)rac_removeObjectsAtIndexes:(NSIndexSet *)indexSet {
[self removeObjectsAtIndexes:indexSet];
}

- (void)rac_replaceObjectsAtIndexes:(NSIndexSet *)indexSet withObjects:(NSArray *)objects {
[self replaceObjectsAtIndexes:indexSet withObjects:objects];
}

- (void)rac_moveObjectAtIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex {
[self moveObjectsAtIndexes:[NSIndexSet indexSetWithIndex:fromIndex] toIndex:toIndex];
}

@end

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
Expand Down
4 changes: 4 additions & 0 deletions ReactiveCocoaFramework/ReactiveCocoa/NSSet+RACSupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import <Foundation/Foundation.h>
#import "RACCollection.h"
#import "RACDeprecated.h"

@class RACSequence;
Expand All @@ -21,6 +22,9 @@

@end

@interface NSMutableSet (RACCollectionSupport) <RACCollection>
@end

@interface NSSet (RACSupportDeprecated)

@property (nonatomic, copy, readonly) RACSequence *rac_sequence RACDeprecated("Use -rac_signal instead");
Expand Down
16 changes: 16 additions & 0 deletions ReactiveCocoaFramework/ReactiveCocoa/NSSet+RACSupport.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ - (RACSignal *)rac_signal {

@end

@implementation NSMutableSet (RACCollectionSupport)

- (void)rac_addObjects:(NSArray *)objects {
[self addObjectsFromArray:objects];
}

- (void)rac_removeObjects:(NSArray *)objects {
[self minusSet:[NSSet setWithArray:objects]];
}

- (void)rac_replaceAllObjects:(NSArray *)objects {
[self setSet:[NSSet setWithArray:objects]];
}

@end

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
Expand Down
33 changes: 33 additions & 0 deletions ReactiveCocoaFramework/ReactiveCocoa/NSTableView+RACSupport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// NSTableView+RACSupport.h
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2014-01-01.
// Copyright (c) 2014 GitHub, Inc. All rights reserved.
//

#import <Cocoa/Cocoa.h>

@class RACDisposable;
@class RACSignal;

@interface NSTableView (RACSupport)

/// Automatically inserts, removes, and reloads rows in the table view, based on
/// the ordered collection mutations received from the given signal.
///
/// This method only supports view-based table views.
///
/// orderedMutations - A signal of <RACOrderedCollectionMutation> objects
/// describing the indexes that should be updated in the
/// table view. The actual objects being modified are
/// ignored. This signal should never error.
/// insertionOptions - Options describing how insertions into the table view
/// should be animated.
/// removalOptions - Options describing how removals from the table view
/// should be animated.
///
/// Returns a disposable which can be used to cancel the binding.
- (RACDisposable *)rac_animateOrderedMutations:(RACSignal *)orderedMutations withInsertionAnimation:(NSTableViewAnimationOptions)insertionOptions removalAnimation:(NSTableViewAnimationOptions)removalOptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice if the animations parameters (here and elsewhere) were signals since it seems likely you won't always want animations. But I'm happy to merge this without it for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I designed these APIs to be super simple. It's easy enough to implement this by hand that it'd be totally appropriate to reinvent for special behaviors w.r.t. animations, or updating multiple parts of a UI{Table,Collection}View at once.


@end