Skip to content

Transforms #186

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

Merged
merged 16 commits into from
Jan 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions FirebaseDatabaseUI/FUIArray.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

@import FirebaseDatabase;

#import "FUIArrayDelegate.h"
#import "FUICollection.h"

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -43,30 +43,31 @@ NS_ASSUME_NONNULL_BEGIN
/**
* FUIArray provides an array structure that is synchronized with a Firebase reference or
* query. It is useful for building custom data structures or sources, and provides the base for
* FirebaseDataSource.
* FirebaseDataSource. FUIArray maintains a large amount of internal state, and most of its methods
* are not thread-safe.
*/
@interface FUIArray : NSObject
@interface FUIArray : NSObject <FUICollection>

/**
* The delegate object that array changes are surfaced to, which conforms to the
* @c FUIArrayDelegate protocol.
* @c FUICollectionDelegate protocol.
*/
@property(weak, nonatomic, nullable) id<FUIArrayDelegate> delegate;
@property (weak, nonatomic, nullable) id<FUICollectionDelegate> delegate;

/**
* The query on a Firebase reference that provides data to populate the array.
*/
@property(strong, nonatomic) id<FUIDataObservable> query;
@property (strong, nonatomic) id<FUIDataObservable> query;

/**
* The number of objects in the array.
*/
@property(nonatomic, readonly) NSUInteger count;
@property (nonatomic, readonly) NSUInteger count;

/**
* The items currently in the array.
*/
@property(nonatomic, readonly, copy) NSArray *items;
@property (nonatomic, readonly, copy) NSArray *items;

#pragma mark - Initializer methods

Expand All @@ -78,7 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
* @return A FirebaseArray instance
*/
- (instancetype)initWithQuery:(id<FUIDataObservable>)query
delegate:(nullable id<FUIArrayDelegate>)delegate NS_DESIGNATED_INITIALIZER;
delegate:(nullable id<FUICollectionDelegate>)delegate NS_DESIGNATED_INITIALIZER;

/**
* Initalizes FirebaseArray with a Firebase query (FIRDatabaseQuery) or database reference
Expand All @@ -102,9 +103,9 @@ NS_ASSUME_NONNULL_BEGIN
/**
* Returns an object at a specific index in the array.
* @param index The index of the item to retrieve
* @return The object at the given index
* @return The snapshot at the given index
*/
- (id)objectAtIndex:(NSUInteger)index;
- (FIRDataSnapshot *)snapshotAtIndex:(NSInteger)index;

/**
* Returns a Firebase reference for an object at a specific index in the array.
Expand Down Expand Up @@ -135,6 +136,38 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (NSUInteger)indexForKey:(NSString *)key;

/**
* Called when the Firebase query sends a FIRDataEventTypeChildAdded event. Override this
* to provide custom insertion logic. Don't call this method directly.
* @param snap The snapshot that was inserted.
* @param previous The key of the sibling preceding the inserted snapshot.
*/
- (void)insertSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(nullable NSString *)previous;

/**
* Called when the Firebase query sends a FIRDataEventTypeChildRemoved event. Override this
* to provide custom removal logic. Don't call this method directly.
* @param snap The snapshot that was removed.
* @param previous The key of the sibling preceding the removed snapshot.
*/
- (void)removeSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(nullable NSString *)previous;

/**
* Called when the Firebase query sends a FIRDataEventTypeChildChanged event. Override this
* to provide custom on change logic. Don't call this method directly.
* @param snap The snapshot whose value was changed.
* @param previous The key of the sibling preceding the changed snapshot.
*/
- (void)changeSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(nullable NSString *)previous;

/**
* Called when the Firebase query sends a FIRDataEventTypeChildMoved event. Override this
* to provide custom move logic. Don't call this method directly.
* @param snap The snapshot that was moved.
* @param previous The key of the sibling preceding the moved snapshot at its new location.
*/
- (void)moveSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(nullable NSString *)previous;

@end

NS_ASSUME_NONNULL_END
194 changes: 132 additions & 62 deletions FirebaseDatabaseUI/FUIArray.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,36 @@ @interface FUIArray ()
/**
* The backing collection that holds all of the array's data.
*/
@property(strong, nonatomic) NSMutableArray<FIRDataSnapshot *> *snapshots;
@property (strong, nonatomic) NSMutableArray<FIRDataSnapshot *> *snapshots;

/**
* A set containing the query observer handles that should be released when
* this array is freed.
*/
@property(strong, nonatomic) NSMutableSet<NSNumber *> *handles;
@property (strong, nonatomic) NSMutableSet<NSNumber *> *handles;

/**
* Set to YES when any event that isn't a value event is received; set
* back to NO when receiving a value event.
* Used to keep track of whether or not the array is updating so consumers
* can more easily batch updates.
*/
@property (nonatomic, assign) BOOL isSendingUpdates;

@end

@implementation FUIArray

#pragma mark - Initializer methods

- (instancetype)initWithQuery:(FIRDatabaseQuery *)query delegate:(id<FUIArrayDelegate>)delegate {
- (instancetype)initWithQuery:(FIRDatabaseQuery *)query delegate:(id<FUICollectionDelegate>)delegate {
NSParameterAssert(query != nil);
self = [super init];
if (self) {
self.snapshots = [NSMutableArray array];
self.query = query;
self.handles = [NSMutableSet setWithCapacity:4];
self.delegate = delegate;
[self initListeners];
}
return self;
}
Expand All @@ -63,94 +70,94 @@ + (instancetype)arrayWithQuery:(id<FUIDataObservable>)query {
#pragma mark - Memory management methods

- (void)dealloc {
for (NSNumber *handle in _handles) {
[_query removeObserverWithHandle:handle.unsignedIntegerValue];
}
[self invalidate];
}

#pragma mark - Private API methods

- (void)initListeners {
- (void)observeQuery {
if (self.handles.count == 5) { /* don't duplicate observers */ return; }
FIRDatabaseHandle handle;
handle = [self.query observeEventType:FIRDataEventTypeChildAdded
andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousChildKey) {
NSUInteger index = 0;
if (previousChildKey != nil) {
index = [self indexForKey:previousChildKey] + 1;
}

[self.snapshots insertObject:snapshot atIndex:index];

if ([self.delegate respondsToSelector:@selector(array:didAddObject:atIndex:)]) {
[self.delegate array:self didAddObject:snapshot atIndex:index];
}
[self didUpdate];
[self insertSnapshot:snapshot withPreviousChildKey:previousChildKey];
}
withCancelBlock:^(NSError *error) {
if ([self.delegate respondsToSelector:@selector(array:queryCancelledWithError:)]) {
[self.delegate array:self queryCancelledWithError:error];
}
[self raiseError:error];
}];
[_handles addObject:@(handle)];

handle = [self.query observeEventType:FIRDataEventTypeChildChanged
andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousChildKey) {
NSUInteger index = [self indexForKey:snapshot.key];

[self.snapshots replaceObjectAtIndex:index withObject:snapshot];

if ([self.delegate respondsToSelector:@selector(array:didChangeObject:atIndex:)]) {
[self.delegate array:self didChangeObject:snapshot atIndex:index];
}
[self didUpdate];
[self changeSnapshot:snapshot withPreviousChildKey:previousChildKey];
}
withCancelBlock:^(NSError *error) {
if ([self.delegate respondsToSelector:@selector(array:queryCancelledWithError:)]) {
[self.delegate array:self queryCancelledWithError:error];
}
[self raiseError:error];
}];
[_handles addObject:@(handle)];

handle = [self.query observeEventType:FIRDataEventTypeChildRemoved
andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousSiblingKey) {
NSUInteger index = [self indexForKey:snapshot.key];

[self.snapshots removeObjectAtIndex:index];

if ([self.delegate respondsToSelector:@selector(array:didRemoveObject:atIndex:)]) {
[self.delegate array:self didRemoveObject:snapshot atIndex:index];
}
[self didUpdate];
[self removeSnapshot:snapshot withPreviousChildKey:previousSiblingKey];
}
withCancelBlock:^(NSError *error) {
if ([self.delegate respondsToSelector:@selector(array:queryCancelledWithError:)]) {
[self.delegate array:self queryCancelledWithError:error];
}
[self raiseError:error];
}];
[_handles addObject:@(handle)];

handle = [self.query observeEventType:FIRDataEventTypeChildMoved
andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousChildKey) {
NSUInteger fromIndex = [self indexForKey:snapshot.key];
[self.snapshots removeObjectAtIndex:fromIndex];

NSUInteger toIndex = 0;
if (previousChildKey != nil) {
NSUInteger prevIndex = [self indexForKey:previousChildKey];
if (prevIndex != NSNotFound) {
toIndex = prevIndex + 1;
}
}

[self.snapshots insertObject:snapshot atIndex:toIndex];

if ([self.delegate respondsToSelector:@selector(array:didMoveObject:fromIndex:toIndex:)]) {
[self.delegate array:self didMoveObject:snapshot fromIndex:fromIndex toIndex:toIndex];
}
[self didUpdate];
[self moveSnapshot:snapshot withPreviousChildKey:previousChildKey];
}
withCancelBlock:^(NSError *error) {
if ([self.delegate respondsToSelector:@selector(array:queryCancelledWithError:)]) {
[self.delegate array:self queryCancelledWithError:error];
}
[self raiseError:error];
}];
[_handles addObject:@(handle)];

handle = [self.query observeEventType:FIRDataEventTypeValue
andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousChildKey) {
[self didFinishUpdates];
}
withCancelBlock:^(NSError *error) {
[self raiseError:error];
}];
[_handles addObject:@(handle)];
}

// Must be called from every non-value event listener in order to work correctly.
- (void)didUpdate {
if (self.isSendingUpdates) {
return;
}
self.isSendingUpdates = YES;
if ([self.delegate respondsToSelector:@selector(arrayDidBeginUpdates:)]) {
[self.delegate arrayDidBeginUpdates:self];
}
}

// Must be called from a value event listener.
- (void)didFinishUpdates {
if (!self.isSendingUpdates) { /* This is probably an error */ return; }
self.isSendingUpdates = NO;
if ([self.delegate respondsToSelector:@selector(arrayDidEndUpdates:)]) {
[self.delegate arrayDidEndUpdates:self];
}
}

- (void)raiseError:(NSError *)error {
if ([self.delegate respondsToSelector:@selector(array:queryCancelledWithError:)]) {
[self.delegate array:self queryCancelledWithError:error];
}
}

- (void)invalidate {
for (NSNumber *handle in _handles) {
[_query removeObserverWithHandle:handle.unsignedIntegerValue];
}
}

- (NSUInteger)indexForKey:(NSString *)key {
Expand All @@ -164,6 +171,69 @@ - (NSUInteger)indexForKey:(NSString *)key {
return NSNotFound;
}

- (void)insertSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(NSString *)previous {
NSUInteger index = 0;
if (previous != nil) {
index = [self indexForKey:previous] + 1;
}

[self.snapshots insertObject:snap atIndex:index];

if ([self.delegate respondsToSelector:@selector(array:didAddObject:atIndex:)]) {
[self.delegate array:self didAddObject:snap atIndex:index];
}
}

- (void)removeSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(NSString *)previous {
NSUInteger index = [self indexForKey:snap.key];

[self.snapshots removeObjectAtIndex:index];

if ([self.delegate respondsToSelector:@selector(array:didRemoveObject:atIndex:)]) {
[self.delegate array:self didRemoveObject:snap atIndex:index];
}
}

- (void)changeSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(NSString *)previous {
NSUInteger index = [self indexForKey:snap.key];

[self.snapshots replaceObjectAtIndex:index withObject:snap];

if ([self.delegate respondsToSelector:@selector(array:didChangeObject:atIndex:)]) {
[self.delegate array:self didChangeObject:snap atIndex:index];
}
}

- (void)moveSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(NSString *)previous {
NSUInteger fromIndex = [self indexForKey:snap.key];
[self.snapshots removeObjectAtIndex:fromIndex];

NSUInteger toIndex = 0;
if (previous != nil) {
NSUInteger prevIndex = [self indexForKey:previous];
if (prevIndex != NSNotFound) {
toIndex = prevIndex + 1;
}
}
[self.snapshots insertObject:snap atIndex:toIndex];

if ([self.delegate respondsToSelector:@selector(array:didMoveObject:fromIndex:toIndex:)]) {
[self.delegate array:self didMoveObject:snap fromIndex:fromIndex toIndex:toIndex];
}
}

- (void)removeSnapshotAtIndex:(NSUInteger)index {
[self.snapshots removeObjectAtIndex:index];
}

- (void)insertSnapshot:(FIRDataSnapshot *)snap atIndex:(NSUInteger)index {
[self.snapshots insertObject:snap atIndex:index];
}

- (void)addSnapshot:(FIRDataSnapshot *)snap {
[self.snapshots addObject:snap];
}

#pragma mark - Public API methods

- (NSArray *)items {
Expand All @@ -174,7 +244,7 @@ - (NSUInteger)count {
return [self.snapshots count];
}

- (FIRDataSnapshot *)objectAtIndex:(NSUInteger)index {
- (FIRDataSnapshot *)snapshotAtIndex:(NSInteger)index {
return (FIRDataSnapshot *)[self.snapshots objectAtIndex:index];
}

Expand All @@ -183,7 +253,7 @@ - (FIRDatabaseReference *)refForIndex:(NSUInteger)index {
}

- (id)objectAtIndexedSubscript:(NSUInteger)index {
return [self objectAtIndex:index];
return [self snapshotAtIndex:index];
}

- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)index{
Expand Down
Loading