diff --git a/FirebaseDatabaseUI/FUIArray.h b/FirebaseDatabaseUI/FUIArray.h index 4ec3acb0056..79fa9576a68 100644 --- a/FirebaseDatabaseUI/FUIArray.h +++ b/FirebaseDatabaseUI/FUIArray.h @@ -20,7 +20,7 @@ @import FirebaseDatabase; -#import "FUIArrayDelegate.h" +#import "FUICollection.h" NS_ASSUME_NONNULL_BEGIN @@ -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 /** * The delegate object that array changes are surfaced to, which conforms to the - * @c FUIArrayDelegate protocol. + * @c FUICollectionDelegate protocol. */ -@property(weak, nonatomic, nullable) id delegate; +@property (weak, nonatomic, nullable) id delegate; /** * The query on a Firebase reference that provides data to populate the array. */ -@property(strong, nonatomic) id query; +@property (strong, nonatomic) id 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 @@ -78,7 +79,7 @@ NS_ASSUME_NONNULL_BEGIN * @return A FirebaseArray instance */ - (instancetype)initWithQuery:(id)query - delegate:(nullable id)delegate NS_DESIGNATED_INITIALIZER; + delegate:(nullable id)delegate NS_DESIGNATED_INITIALIZER; /** * Initalizes FirebaseArray with a Firebase query (FIRDatabaseQuery) or database reference @@ -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. @@ -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 diff --git a/FirebaseDatabaseUI/FUIArray.m b/FirebaseDatabaseUI/FUIArray.m index a1369fd1e71..19fe039ce42 100755 --- a/FirebaseDatabaseUI/FUIArray.m +++ b/FirebaseDatabaseUI/FUIArray.m @@ -25,13 +25,21 @@ @interface FUIArray () /** * The backing collection that holds all of the array's data. */ -@property(strong, nonatomic) NSMutableArray *snapshots; +@property (strong, nonatomic) NSMutableArray *snapshots; /** * A set containing the query observer handles that should be released when * this array is freed. */ -@property(strong, nonatomic) NSMutableSet *handles; +@property (strong, nonatomic) NSMutableSet *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 @@ -39,7 +47,7 @@ @implementation FUIArray #pragma mark - Initializer methods -- (instancetype)initWithQuery:(FIRDatabaseQuery *)query delegate:(id)delegate { +- (instancetype)initWithQuery:(FIRDatabaseQuery *)query delegate:(id)delegate { NSParameterAssert(query != nil); self = [super init]; if (self) { @@ -47,7 +55,6 @@ - (instancetype)initWithQuery:(FIRDatabaseQuery *)query delegate:(id)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 { @@ -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 { @@ -174,7 +244,7 @@ - (NSUInteger)count { return [self.snapshots count]; } -- (FIRDataSnapshot *)objectAtIndex:(NSUInteger)index { +- (FIRDataSnapshot *)snapshotAtIndex:(NSInteger)index { return (FIRDataSnapshot *)[self.snapshots objectAtIndex:index]; } @@ -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{ diff --git a/FirebaseDatabaseUI/FUIArrayDelegate.h b/FirebaseDatabaseUI/FUICollection.h similarity index 51% rename from FirebaseDatabaseUI/FUIArrayDelegate.h rename to FirebaseDatabaseUI/FUICollection.h index 7c3d9ef8570..1f5b7655595 100644 --- a/FirebaseDatabaseUI/FUIArrayDelegate.h +++ b/FirebaseDatabaseUI/FUICollection.h @@ -18,57 +18,109 @@ // clang-format on +@import Foundation; + @class FUIArray; +@protocol FUICollectionDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A protocol representing a collection of objects from Firebase Database. + */ +@protocol FUICollection + +@property (nonatomic, readonly, copy) NSArray *items; + +@property (weak, nonatomic, nullable) id delegate; + +/** + * The number of objects in the collection. + */ +@property (nonatomic, readonly) NSUInteger count; + +/** + * The @c FIRDataSnapshot at the given index. May raise fatal errors + * if the index is out of bounds. + * @param index The index of a snapshot. + */ +- (FIRDataSnapshot *)snapshotAtIndex:(NSInteger)index; + +/** + * Calling this makes the array begin observing updates from its query. + * Before this call is made the array is inert and doesn't do anything. + */ +- (void)observeQuery; + +/** + * Cancels all active observations. The array may be reused after this + * is called by calling @c observeQuery again. + */ +- (void)invalidate; + +@end /** * A protocol to allow instances of FUIArray to raise events through a * delegate. Raises all Firebase events except FIRDataEventTypeValue. */ -@protocol FUIArrayDelegate +@protocol FUICollectionDelegate @optional +/** + * Called before any other events are sent. + */ +- (void)arrayDidBeginUpdates:(id)collection; + +/** + * Called after all updates have finished. + */ +- (void)arrayDidEndUpdates:(id)collection; + /** * Delegate method which is called whenever an object is added to an FUIArray. - * On a FUIArray synchronized to a Firebase reference, this corresponds to a + * On a FUIArray synchronized to a Firebase reference, this corresponds to an * @c FIRDataEventTypeChildAdded event being raised. * @param object The object added to the FUIArray * @param index The index the child was added at */ -- (void)array:(FUIArray *)array didAddObject:(id)object atIndex:(NSUInteger)index; +- (void)array:(id)array didAddObject:(id)object atIndex:(NSUInteger)index; /** * Delegate method which is called whenever an object is changed in an * FUIArray. On a FUIArray synchronized to a Firebase reference, this - * corresponds to a @c FIRDataEventTypeChildChanged event being raised. + * corresponds to an @c FIRDataEventTypeChildChanged event being raised. * @param object The object that changed in the FUIArray * @param index The index the child was changed at */ -- (void)array:(FUIArray *)array didChangeObject:(id)object atIndex:(NSUInteger)index; +- (void)array:(id)array didChangeObject:(id)object atIndex:(NSUInteger)index; /** * Delegate method which is called whenever an object is removed from an * FUIArray. On a FUIArray synchronized to a Firebase reference, this - * corresponds to a @c FIRDataEventTypeChildRemoved event being raised. + * corresponds to an @c FIRDataEventTypeChildRemoved event being raised. * @param object The object removed from the FUIArray * @param index The index the child was removed at */ -- (void)array:(FUIArray *)array didRemoveObject:(id)object atIndex:(NSUInteger)index; +- (void)array:(id)array didRemoveObject:(id)object atIndex:(NSUInteger)index; /** * Delegate method which is called whenever an object is moved within a * FUIArray. On a FUIArray synchronized to a Firebase reference, this - * corresponds to a @c FIRDataEventTypeChildMoved event being raised. + * corresponds to an @c FIRDataEventTypeChildMoved event being raised. * @param object The object that has moved locations in the FUIArray * @param fromIndex The index the child is being moved from * @param toIndex The index the child is being moved to */ -- (void)array:(FUIArray *)array didMoveObject:(id)object fromIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex; +- (void)array:(id)array didMoveObject:(id)object fromIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex; /** * Delegate method which is called whenever the backing query is canceled. * @param error the error that was raised */ -- (void)array:(FUIArray *)array queryCancelledWithError:(NSError *)error; +- (void)array:(id)array queryCancelledWithError:(NSError *)error; @end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseDatabaseUI/FUICollectionViewDataSource.h b/FirebaseDatabaseUI/FUICollectionViewDataSource.h index 95f7e946aa6..119b6f891a0 100644 --- a/FirebaseDatabaseUI/FUICollectionViewDataSource.h +++ b/FirebaseDatabaseUI/FUICollectionViewDataSource.h @@ -51,6 +51,26 @@ NS_ASSUME_NONNULL_BEGIN /** * Initialize an instance of FUICollectionViewDataSource that populates * UICollectionViewCells with FIRDataSnapshots. + * @param collection A FUICollection that the data source uses to pull snapshots + * from Firebase Database. + * @param view An instance of a UICollectionView to bind to. This view + * is not retained by its data source. + * @param populateCell A closure used by the data source to create the cells that + * are displayed in the collection view. This closure is retained by the data + * source, so if you capture self in the closure and also claim ownership of the + * data source, be sure to avoid retain cycles by capturing a weak reference to self. + * @return An instance of FUICollectionViewDataSource that populates + * UICollectionViewCells with FIRDataSnapshots. + */ +- (instancetype)initWithCollection:(id)collection + view:(UICollectionView *)view + populateCell:(UICollectionViewCell *(^)(UICollectionView *collectionView, + NSIndexPath *indexPath, + FIRDataSnapshot *object))populateCell NS_DESIGNATED_INITIALIZER; + +/** + * Initialize an unsorted instance of FUICollectionViewDataSource that populates + * UICollectionViewCells with FIRDataSnapshots. * @param query A Firebase query to bind the data source to. * @param collectionView An instance of a UICollectionView to bind to. This view * is not retained by its data source. @@ -65,9 +85,9 @@ NS_ASSUME_NONNULL_BEGIN view:(UICollectionView *)collectionView populateCell:(UICollectionViewCell *(^)(UICollectionView *collectionView, NSIndexPath *indexPath, - FIRDataSnapshot *object))populateCell NS_DESIGNATED_INITIALIZER; + FIRDataSnapshot *object))populateCell; -- (instancetype)initWithArray:(FUIArray *)array NS_UNAVAILABLE; +- (instancetype)initWithCollection:(id)collection NS_UNAVAILABLE; @end diff --git a/FirebaseDatabaseUI/FUICollectionViewDataSource.m b/FirebaseDatabaseUI/FUICollectionViewDataSource.m index d446ceea204..467dac46701 100644 --- a/FirebaseDatabaseUI/FUICollectionViewDataSource.m +++ b/FirebaseDatabaseUI/FUICollectionViewDataSource.m @@ -26,21 +26,29 @@ @implementation FUICollectionViewDataSource #pragma mark - FUIDataSource initializer methods +- (instancetype)initWithCollection:(id)collection + view:(UICollectionView *)view + populateCell:(UICollectionViewCell * (^)(UICollectionView *, + NSIndexPath *, + FIRDataSnapshot *))populateCell { + self = [super initWithCollection:collection]; + if (self) { + _collectionView = view; + _populateCellAtIndexPath = populateCell; + } + return self; +} + - (instancetype)initWithQuery:(FIRDatabaseQuery *)query view:(UICollectionView *)collectionView populateCell:(UICollectionViewCell *(^)(UICollectionView *, NSIndexPath *, FIRDataSnapshot *))populateCell { FUIArray *array = [[FUIArray alloc] initWithQuery:query]; - self = [super initWithArray:array]; - if (self) { - _collectionView = collectionView; - _populateCellAtIndexPath = populateCell; - } - return self; + return [self initWithCollection:array view:collectionView populateCell:populateCell]; } -#pragma mark - FUIArrayDelegate methods +#pragma mark - FUICollectionDelegate methods - (void)array:(FUIArray *)array didAddObject:(id)object atIndex:(NSUInteger)index { [self.collectionView diff --git a/FirebaseDatabaseUI/FUIDataSource.h b/FirebaseDatabaseUI/FUIDataSource.h index 508cdbbf591..9e84383afc9 100644 --- a/FirebaseDatabaseUI/FUIDataSource.h +++ b/FirebaseDatabaseUI/FUIDataSource.h @@ -32,7 +32,7 @@ * subclasses need as well as several methods that pass through to the instance * of FirebaseArray. */ -@interface FUIDataSource : NSObject +@interface FUIDataSource : NSObject /** * The items in the data source. @@ -40,23 +40,21 @@ @property (nonatomic, readonly, copy) NSArray *items; /** - * Pass through of [FirebaseArray count]. + * The number of items in the receiver's collection. */ @property (nonatomic, readonly) NSUInteger count; -- (instancetype)initWithArray:(FUIArray *)array NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - /** - * Pass through of [FirebaseArray objectAtIndex:]. + * Takes an FUICollection and immediately starts observing it. */ -- (id)objectAtIndex:(NSUInteger)index; +- (instancetype)initWithCollection:(id)collection NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; /** - * Pass through of [FirebaseArray refForIndex:]. + * Returns the snapshot at the given index in the receiver's collection. */ -- (FIRDatabaseReference *)refForIndex:(NSUInteger)index; +- (FIRDataSnapshot *)objectAtIndex:(NSUInteger)index; /** * Provides a block which is called when the backing array cancels its query. diff --git a/FirebaseDatabaseUI/FUIDataSource.m b/FirebaseDatabaseUI/FUIDataSource.m index 458bb9a9db3..5ec9d4d109b 100644 --- a/FirebaseDatabaseUI/FUIDataSource.m +++ b/FirebaseDatabaseUI/FUIDataSource.m @@ -27,7 +27,7 @@ @interface FUIDataSource () /** * The FirebaseArray which backs the instance of the datasource. */ -@property(strong, nonatomic, nonnull) FUIArray *array; +@property(strong, nonatomic, nonnull) id collection; @end @@ -35,11 +35,12 @@ @implementation FUIDataSource #pragma mark - Initializer methods -- (instancetype)initWithArray:(FUIArray *)array { +- (instancetype)initWithCollection:(id)collection { self = [super init]; if (self) { - _array = array; - _array.delegate = self; + _collection = collection; + _collection.delegate = self; + [_collection observeQuery]; } return self; } @@ -47,26 +48,22 @@ - (instancetype)initWithArray:(FUIArray *)array { #pragma mark - API methods - (NSArray *)items { - return [self.array.items copy]; + return [self.collection.items copy]; } - (NSUInteger)count { - return self.array.count; + return self.collection.count; } -- (id)objectAtIndex:(NSUInteger)index { - return [self.array objectAtIndex:index]; -} - -- (FIRDatabaseReference *)refForIndex:(NSUInteger)index { - return [self.array refForIndex:index]; +- (FIRDataSnapshot *)objectAtIndex:(NSUInteger)index { + return [self.collection snapshotAtIndex:index]; } - (void)cancelWithBlock:(void (^)(NSError *))block { self.cancelBlock = block; } -#pragma mark - FUIArrayDelegate methods +#pragma mark - FUICollectionDelegate methods - (void)canceledWithError:(NSError *)error { if (self.cancelBlock != nil) { diff --git a/FirebaseDatabaseUI/FUIIndexArray.m b/FirebaseDatabaseUI/FUIIndexArray.m index de5da718b6c..efee6bbdaef 100644 --- a/FirebaseDatabaseUI/FUIIndexArray.m +++ b/FirebaseDatabaseUI/FUIIndexArray.m @@ -21,7 +21,7 @@ #import "FUIIndexArray.h" #import "FUIQueryObserver.h" -@interface FUIIndexArray () +@interface FUIIndexArray () @property (nonatomic, readonly) id index; @property (nonatomic, readonly) id data; @@ -71,6 +71,7 @@ - (instancetype)initWithIndex:(id)index - (void)observeQueries { _indexArray = [[FUIArray alloc] initWithQuery:self.index delegate:self]; + [_indexArray observeQuery]; } - (NSArray *)items { diff --git a/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.h b/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.h index e5801cda713..bc2093cb4af 100644 --- a/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.h +++ b/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.h @@ -101,11 +101,11 @@ didFailLoadAtIndex:(NSUInteger)index * view is in use. */ - (FUIIndexCollectionViewDataSource *)bindToIndexedQuery:(FIRDatabaseQuery *)index - data:(FIRDatabaseReference *)data - delegate:(id)delegate - populateCell:(UICollectionViewCell *(^)(UICollectionView *view, - NSIndexPath *indexPath, - FIRDataSnapshot *_Nullable snap))populateCell; + data:(FIRDatabaseReference *)data + delegate:(id)delegate + populateCell:(UICollectionViewCell *(^)(UICollectionView *view, + NSIndexPath *indexPath, + FIRDataSnapshot *_Nullable snap))populateCell __attribute__((warn_unused_result)); @end diff --git a/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.m b/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.m index 9479ad7a1e4..923c004af08 100644 --- a/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.m +++ b/FirebaseDatabaseUI/FUIIndexCollectionViewDataSource.m @@ -123,11 +123,11 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView @implementation UICollectionView (FUIIndexCollectionViewDataSource) - (FUIIndexCollectionViewDataSource *)bindToIndexedQuery:(FIRDatabaseQuery *)index - data:(FIRDatabaseReference *)data - delegate:(id)delegate - populateCell:(UICollectionViewCell *(^)(UICollectionView *, - NSIndexPath *, - FIRDataSnapshot *))populateCell { + data:(FIRDatabaseReference *)data + delegate:(id)delegate + populateCell:(UICollectionViewCell *(^)(UICollectionView *, + NSIndexPath *, + FIRDataSnapshot *))populateCell { FUIIndexCollectionViewDataSource *dataSource = [[FUIIndexCollectionViewDataSource alloc] initWithIndex:index data:data diff --git a/FirebaseDatabaseUI/FUIIndexTableViewDataSource.h b/FirebaseDatabaseUI/FUIIndexTableViewDataSource.h index 1ad87f05f49..9f035378091 100644 --- a/FirebaseDatabaseUI/FUIIndexTableViewDataSource.h +++ b/FirebaseDatabaseUI/FUIIndexTableViewDataSource.h @@ -104,11 +104,11 @@ didFailLoadAtIndex:(NSUInteger)index * view is in use. */ - (FUIIndexTableViewDataSource *)bindToIndexedQuery:(FIRDatabaseQuery *)index - data:(FIRDatabaseReference *)data - delegate:(id)delegate - populateCell:(UITableViewCell *(^)(UITableView *view, - NSIndexPath *indexPath, - FIRDataSnapshot *_Nullable snap))populateCell; + data:(FIRDatabaseReference *)data + delegate:(id)delegate + populateCell:(UITableViewCell *(^)(UITableView *view, + NSIndexPath *indexPath, + FIRDataSnapshot *_Nullable snap))populateCell __attribute__((warn_unused_result)); @end diff --git a/FirebaseDatabaseUI/FUIIndexTableViewDataSource.m b/FirebaseDatabaseUI/FUIIndexTableViewDataSource.m index 4ae550c058f..33f188a5e25 100644 --- a/FirebaseDatabaseUI/FUIIndexTableViewDataSource.m +++ b/FirebaseDatabaseUI/FUIIndexTableViewDataSource.m @@ -48,8 +48,8 @@ - (instancetype)initWithIndex:(FIRDatabaseQuery *)indexQuery self = [super init]; if (self != nil) { _array = [[FUIIndexArray alloc] initWithIndex:indexQuery - data:dataQuery - delegate:self]; + data:dataQuery + delegate:self]; _tableView = tableView; tableView.dataSource = self; _populateCell = populateCell; @@ -139,17 +139,17 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger @implementation UITableView (FUIIndexTableViewDataSource) - (FUIIndexTableViewDataSource *)bindToIndexedQuery:(FIRDatabaseQuery *)index - data:(FIRDatabaseReference *)data - delegate:(id)delegate - populateCell:(UITableViewCell *(^)(UITableView *, - NSIndexPath *, - FIRDataSnapshot *))populateCell { + data:(FIRDatabaseReference *)data + delegate:(id)delegate + populateCell:(UITableViewCell *(^)(UITableView *, + NSIndexPath *, + FIRDataSnapshot *))populateCell { FUIIndexTableViewDataSource *dataSource = [[FUIIndexTableViewDataSource alloc] initWithIndex:index - data:data - tableView:self - delegate:delegate - populateCell:populateCell]; + data:data + tableView:self + delegate:delegate + populateCell:populateCell]; self.dataSource = dataSource; return dataSource; } diff --git a/FirebaseDatabaseUI/FUIQueryObserver.m b/FirebaseDatabaseUI/FUIQueryObserver.m index 53fe1c083f1..456e48217fb 100644 --- a/FirebaseDatabaseUI/FUIQueryObserver.m +++ b/FirebaseDatabaseUI/FUIQueryObserver.m @@ -44,9 +44,9 @@ - (instancetype)initWithQuery:(id)query { } + (FUIQueryObserver *)observerForQuery:(id)query - completion:(void (^)(FUIQueryObserver *obs, - FIRDataSnapshot *snap, - NSError *error))completion { + completion:(void (^)(FUIQueryObserver *obs, + FIRDataSnapshot *snap, + NSError *error))completion { FUIQueryObserver *obs = [[FUIQueryObserver alloc] initWithQuery:query]; void (^observerBlock)(FIRDataSnapshot *, NSString *) = ^(FIRDataSnapshot *snap, diff --git a/FirebaseDatabaseUI/FUISortedArray.h b/FirebaseDatabaseUI/FUISortedArray.h new file mode 100644 index 00000000000..e4ea2924bf9 --- /dev/null +++ b/FirebaseDatabaseUI/FUISortedArray.h @@ -0,0 +1,48 @@ +// +// Copyright (c) 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@import FirebaseDatabase; + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FUISortedArray : FUIArray + +/** + * A copy of the snapshots currently in the array. + */ +@property (nonatomic, readonly, copy) NSArray *items; + +- (instancetype)initWithQuery:(id)query NS_UNAVAILABLE; +- (instancetype)initWithQuery:(id)query + delegate:(nullable id)delegate NS_UNAVAILABLE; + +/** + * Initializes a sorted collection. + * @param query The query the receiver uses to pull updates from Firebase Database. + * @param delegate The delegate object that should receive events from the array. + * @param sortDescriptor The closure used by the array to sort its contents. This + * block must always return consistent results or the array may raise a fatal error. + */ +- (instancetype)initWithQuery:(id)query + delegate:(nullable id)delegate + sortDescriptor:(NSComparisonResult (^)(FIRDataSnapshot *left, + FIRDataSnapshot *right))sortDescriptor NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseDatabaseUI/FUISortedArray.m b/FirebaseDatabaseUI/FUISortedArray.m new file mode 100644 index 00000000000..873c55d651e --- /dev/null +++ b/FirebaseDatabaseUI/FUISortedArray.m @@ -0,0 +1,157 @@ +// +// Copyright (c) 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "FUISortedArray.h" + +@interface FUISortedArray () + +/** + * A closure used to sort the downloaded contents from the Firebase query. + */ +@property (nonatomic, copy, nonnull) NSComparisonResult (^sortDescriptor)(FIRDataSnapshot *, FIRDataSnapshot *); + +/** + * The backing collection that holds all of the array's data. + */ +@property (strong, nonatomic) NSMutableArray *snapshots; + +/** + * A set containing the query observer handles that should be released when + * this array is freed. + */ +@property(strong, nonatomic) NSMutableSet *handles; + +@end + +@implementation FUISortedArray +// Cheating at subclassing, but this @dynamic avoids +// duplicating storage without exposing mutability publicly +@dynamic snapshots; + +- (instancetype)initWithQuery:(FIRDatabaseQuery *)query + delegate:(id)delegate + sortDescriptor:(NSComparisonResult (^)(FIRDataSnapshot *, + FIRDataSnapshot *))sortDescriptor { + self = [super initWithQuery:query delegate:delegate]; + if (self != nil) { + _sortDescriptor = sortDescriptor; + _handles = [NSMutableSet setWithCapacity:4]; + } + return self; +} + +- (void)insertSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(NSString *)previous { + NSInteger index = [self insertSnapshot:snap]; + if ([self.delegate respondsToSelector:@selector(array:didAddObject:atIndex:)]) { + [self.delegate array:self didAddObject:snap atIndex:index]; + } +} + +- (void)removeSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(NSString *)previous { + NSInteger index = [self indexForKey:snap.key]; + if (index == NSNotFound) { /* error */ return; } + + [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 { + // Remove and re-insert to maintain sortedness. There are faster ways + // to do this but idgaf + NSInteger index = [self indexForKey:snap.key]; + if (index == NSNotFound) { /* error */ return; } + + // Since changes can change ordering, model changes as a deletion and an insertion. + FIRDataSnapshot *removed = [self snapshotAtIndex:index]; + [self.snapshots removeObjectAtIndex:index]; + if ([self.delegate respondsToSelector:@selector(array:didRemoveObject:atIndex:)]) { + [self.delegate array:self didRemoveObject:removed atIndex:index]; + } + + NSInteger newIndex = [self insertSnapshot:snap]; + if ([self.delegate respondsToSelector:@selector(array:didAddObject:atIndex:)]) { + [self.delegate array:self didAddObject:snap atIndex:newIndex]; + } +} + +- (void)moveSnapshot:(FIRDataSnapshot *)snap withPreviousChildKey:(NSString *)previous { + // Ignore this event, since we do our own ordering. +} + +- (NSArray *)items { + return super.items; +} + +- (NSInteger)insertSnapshot:(FIRDataSnapshot *)snapshot { + if (self.count == 0) { + [self.snapshots addObject:snapshot]; + return 0; + } + if (self.count == 1) { + NSComparisonResult result = self.sortDescriptor(snapshot, [self snapshotAtIndex:0]); + switch (result) { + case NSOrderedDescending: + [self.snapshots addObject:snapshot]; + return 1; + default: + [self.snapshots insertObject:snapshot atIndex:0]; + return 0; + } + } + + NSInteger index = self.count / 2; + while (index >= 0 && index <= self.count) { + if (index == 0) { + [self.snapshots insertObject:snapshot atIndex:index]; + return 0; + } + if (index == self.count) { + [self.snapshots addObject:snapshot]; + return index; + } + + // Comparison results are as if the item were to be inserted between the two + // compared objects. + NSComparisonResult left = self.sortDescriptor([self snapshotAtIndex:index - 1], snapshot); + NSComparisonResult right = self.sortDescriptor(snapshot, [self snapshotAtIndex:index]); + + if (left == NSOrderedDescending && right == NSOrderedAscending) { + // look left + index /= 2; + continue; + } else if (left == NSOrderedAscending && right == NSOrderedDescending) { + // look right + index = ((self.count - index) / 2) + index + 1; + continue; + } else if (left == NSOrderedDescending && right == NSOrderedDescending) { + // bad state (array is not sorted to begin with) + NSAssert(NO, @"FUISortedArray %@'s sort descriptor returned inconsistent results!", self); + } else { + // good + [self.snapshots insertObject:snapshot atIndex:index]; + return index; + } + } + // should be unreachable, but compiler has no way of knowing array is + // always supposed to be sorted. + NSAssert(NO, @"Failed to insert new snapshot: Either sortDescriptor returned inconsistent " + @"results or this is a bug in FirebaseUI"); + abort(); +} + +@end diff --git a/FirebaseDatabaseUI/FUITableViewDataSource.h b/FirebaseDatabaseUI/FUITableViewDataSource.h index 8820cca251c..be3af9bdc5e 100644 --- a/FirebaseDatabaseUI/FUITableViewDataSource.h +++ b/FirebaseDatabaseUI/FUITableViewDataSource.h @@ -48,6 +48,26 @@ NS_ASSUME_NONNULL_BEGIN /** * Initialize an instance of FUITableViewDataSource. + * @param collection An FUICollection used by the data source to pull data + * from Firebase Database. + * @param tableView An instance of a UITableView to bind to. This view is + * not retained by the data source. + * @param populateCell A closure used by the data source to create/reuse + * table view cells and populate their content. This closure is retained + * by the data source, so if you capture self in the closure and also claim ownership + * of the data source, be sure to avoid retain cycles by capturing a weak reference to self. + * @return An instance of FUITableViewDataSource. + */ +- (instancetype)initWithCollection:(id)collection + view:(UITableView *)tableView + populateCell:(UITableViewCell *(^)(UITableView *tableView, + NSIndexPath *indexPath, + FIRDataSnapshot *object))populateCell NS_DESIGNATED_INITIALIZER; + + +/** + * Initialize an instance of FUITableViewDataSource with contents ordered + * by the query. * @param query A Firebase query to bind the data source to. * @param tableView An instance of a UITableView to bind to. This view is * not retained by the data source. @@ -61,9 +81,9 @@ NS_ASSUME_NONNULL_BEGIN view:(UITableView *)tableView populateCell:(UITableViewCell *(^)(UITableView *tableView, NSIndexPath *indexPath, - FIRDataSnapshot *object))populateCell NS_DESIGNATED_INITIALIZER; + FIRDataSnapshot *object))populateCell; -- (instancetype)initWithArray:(FUIArray *)array NS_UNAVAILABLE; +- (instancetype)initWithCollection:(id)collection NS_UNAVAILABLE; @end diff --git a/FirebaseDatabaseUI/FUITableViewDataSource.m b/FirebaseDatabaseUI/FUITableViewDataSource.m index cb870118bac..03e76306bae 100644 --- a/FirebaseDatabaseUI/FUITableViewDataSource.m +++ b/FirebaseDatabaseUI/FUITableViewDataSource.m @@ -35,49 +35,57 @@ @implementation FUITableViewDataSource #pragma mark - FUIDataSource initializer methods +- (instancetype)initWithCollection:(id)collection + view:(UITableView *)tableView + populateCell:(UITableViewCell *(^)(UITableView *, + NSIndexPath *, + FIRDataSnapshot *))populateCell { + self = [super initWithCollection:collection]; + if (self != nil) { + self.tableView = tableView; + self.populateCell = populateCell; + } + return self; +} + - (instancetype)initWithQuery:(FIRDatabaseQuery *)query view:(UITableView *)tableView populateCell:(UITableViewCell *(^)(UITableView *, NSIndexPath *, FIRDataSnapshot *))populateCell { FUIArray *array = [[FUIArray alloc] initWithQuery:query]; - self = [super initWithArray:array]; - if (self) { - self.tableView = tableView; - self.populateCell = populateCell; - } - return self; + return [self initWithCollection:array view:tableView populateCell:populateCell]; } -#pragma mark - FUIArrayDelegate methods +#pragma mark - FUICollectionDelegate methods -- (void)array:(FUIArray *)array didAddObject:(id)object atIndex:(NSUInteger)index { +- (void)arrayDidBeginUpdates:(id)collection { [self.tableView beginUpdates]; +} + +- (void)arrayDidEndUpdates:(id)collection { + [self.tableView endUpdates]; +} + +- (void)array:(FUIArray *)array didAddObject:(id)object atIndex:(NSUInteger)index { [self.tableView insertRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:index inSection:0] ] withRowAnimation:UITableViewRowAnimationAutomatic]; - [self.tableView endUpdates]; } - (void)array:(FUIArray *)array didChangeObject:(id)object atIndex:(NSUInteger)index { - [self.tableView beginUpdates]; [self.tableView reloadRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:index inSection:0] ] withRowAnimation:UITableViewRowAnimationAutomatic]; - [self.tableView endUpdates]; } - (void)array:(FUIArray *)array didRemoveObject:(id)object atIndex:(NSUInteger)index { - [self.tableView beginUpdates]; [self.tableView deleteRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:index inSection:0] ] withRowAnimation:UITableViewRowAnimationAutomatic]; - [self.tableView endUpdates]; } - (void)array:(FUIArray *)array didMoveObject:(id)object fromIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex { - [self.tableView beginUpdates]; [self.tableView moveRowAtIndexPath:[NSIndexPath indexPathForRow:fromIndex inSection:0] toIndexPath:[NSIndexPath indexPathForRow:toIndex inSection:0]]; - [self.tableView endUpdates]; } #pragma mark - UITableViewDataSource methods diff --git a/FirebaseDatabaseUI/FirebaseDatabaseUI.h b/FirebaseDatabaseUI/FirebaseDatabaseUI.h index 565eacbf4ce..89aa666a4bd 100644 --- a/FirebaseDatabaseUI/FirebaseDatabaseUI.h +++ b/FirebaseDatabaseUI/FirebaseDatabaseUI.h @@ -26,7 +26,7 @@ FOUNDATION_EXPORT const unsigned char FirebaseDatabaseUIVersionString[]; #import #import #import -#import +#import #import #import #import diff --git a/FirebaseDatabaseUITests/FUIArrayTest.m b/FirebaseDatabaseUITests/FUIArrayTest.m index fe781c1c2a5..93fa1d51c0b 100644 --- a/FirebaseDatabaseUITests/FUIArrayTest.m +++ b/FirebaseDatabaseUITests/FUIArrayTest.m @@ -41,6 +41,7 @@ - (void)setUp { self.observable = [[FUITestObservable alloc] init]; self.firebaseArray = [[FUIArray alloc] initWithQuery:self.observable]; self.firebaseArray.delegate = self.arrayDelegate; + [self.firebaseArray observeQuery]; } - (void)tearDown { @@ -442,4 +443,37 @@ - (void)testArrayMovesElementToStartWithNilPreviousKey { XCTAssert(expectedParametersWereCorrect, @"unexpected parameter in delegate callback"); } +- (void)testArraySendsMessageBeforeAnyUpdates { + __block NSInteger started = 0; + self.arrayDelegate.didStartUpdates = ^{ + started++; // expect this to only ever be incremented once. + }; + [self.observable populateWithCount:10]; + + XCTAssert(started == 1, @"expected array to start updates exactly once"); + + // Send a value event to mark the end of batch updates. + [self.observable sendEvent:FIRDataEventTypeValue withObject:nil previousKey:nil error:nil]; +} + +- (void)testArraySendsMessagesAfterReceivingValueEvent { + __block NSInteger started = 0; + self.arrayDelegate.didStartUpdates = ^{ + started++; // expect this to only ever be incremented once. + }; + [self.observable populateWithCount:10]; + + XCTAssert(started == 1, @"expected array to start updates exactly once"); + + __block NSInteger ended = 0; + self.arrayDelegate.didEndUpdates = ^{ + ended++; + }; + + // Send a value event to mark the end of batch updates. + [self.observable sendEvent:FIRDataEventTypeValue withObject:nil previousKey:nil error:nil]; + + XCTAssert(ended == 1, @"expected array to end updates exactly once"); +} + @end diff --git a/FirebaseDatabaseUITests/FUIDatabaseTestUtils.h b/FirebaseDatabaseUITests/FUIDatabaseTestUtils.h index 9de6c6732c3..510ab909ce0 100644 --- a/FirebaseDatabaseUITests/FUIDatabaseTestUtils.h +++ b/FirebaseDatabaseUITests/FUIDatabaseTestUtils.h @@ -72,7 +72,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)removeAllObservers; -// Sends an event to the observable's observers. +// Sends an event to the observable's observers. This could be more +// robust--currently it depends on the tester knowing what the "correct" +// previousKey is supposed to be. - (void)sendEvent:(FIRDataEventType)event withObject:(nullable FUIFakeSnapshot *)object previousKey:(nullable NSString *)string @@ -89,12 +91,14 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface FUIArrayTestDelegate : NSObject -@property (nonatomic, copy) void (^queryCancelled)(FUIArray *array, NSError *error); -@property (nonatomic, copy) void (^didAddObject)(FUIArray *array, id object, NSUInteger index); -@property (nonatomic, copy) void (^didChangeObject)(FUIArray *array, id object, NSUInteger index); -@property (nonatomic, copy) void (^didRemoveObject)(FUIArray *array, id object, NSUInteger index); -@property (nonatomic, copy) void (^didMoveObject)(FUIArray *array, id object, NSUInteger fromIndex, NSUInteger toIndex); +@interface FUIArrayTestDelegate : NSObject +@property (nonatomic, copy) void (^didStartUpdates)(); +@property (nonatomic, copy) void (^didEndUpdates)(); +@property (nonatomic, copy) void (^queryCancelled)(id array, NSError *error); +@property (nonatomic, copy) void (^didAddObject)(id array, id object, NSUInteger index); +@property (nonatomic, copy) void (^didChangeObject)(id array, id object, NSUInteger index); +@property (nonatomic, copy) void (^didRemoveObject)(id array, id object, NSUInteger index); +@property (nonatomic, copy) void (^didMoveObject)(id array, id object, NSUInteger fromIndex, NSUInteger toIndex); @end @interface FUIIndexArrayTestDelegate : NSObject diff --git a/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m b/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m index 50af17c2554..a3c9cd75673 100644 --- a/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m +++ b/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m @@ -49,8 +49,7 @@ - (BOOL)isEqual:(id)object { } - (NSString *)description { - return [NSString stringWithFormat:@"%@, key: %@, value: %@", - [super description], self.key, self.value]; + return [NSString stringWithFormat:@"", self, self.key, self.value]; } @end @@ -219,25 +218,37 @@ - (void)populateWithCount:(NSUInteger)count { @implementation FUIArrayTestDelegate -- (void)array:(FUIArray *)array didAddObject:(id)object atIndex:(NSUInteger)index { +- (void)arrayDidBeginUpdates:(id)collection { + if (self.didStartUpdates != NULL) { + self.didStartUpdates(); + } +} + +- (void)arrayDidEndUpdates:(id)collection { + if (self.didEndUpdates != NULL) { + self.didEndUpdates(); + } +} + +- (void)array:(id)array didAddObject:(id)object atIndex:(NSUInteger)index { if (self.didAddObject != NULL) { self.didAddObject(array, object, index); } } -- (void)array:(FUIArray *)array didChangeObject:(id)object atIndex:(NSUInteger)index { +- (void)array:(id)array didChangeObject:(id)object atIndex:(NSUInteger)index { if (self.didChangeObject != NULL) { self.didChangeObject(array, object, index); } } -- (void)array:(FUIArray *)array didRemoveObject:(id)object atIndex:(NSUInteger)index { +- (void)array:(id)array didRemoveObject:(id)object atIndex:(NSUInteger)index { if (self.didRemoveObject != NULL) { self.didRemoveObject(array, object, index); } } -- (void)array:(FUIArray *)array didMoveObject:(id)object +- (void)array:(id)array didMoveObject:(id)object fromIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex { if (self.didMoveObject != NULL) { self.didMoveObject(array, object, fromIndex, toIndex); diff --git a/FirebaseDatabaseUITests/FUISortedArrayTest.m b/FirebaseDatabaseUITests/FUISortedArrayTest.m new file mode 100644 index 00000000000..d7266d924a2 --- /dev/null +++ b/FirebaseDatabaseUITests/FUISortedArrayTest.m @@ -0,0 +1,218 @@ +// +// Copyright (c) 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@import XCTest; +@import FirebaseDatabaseUI; + +#import "FUISortedArray.h" + +#import "FUIDatabaseTestUtils.h" + +@interface FUISortedArrayTest : XCTestCase + +@property (nonatomic, nullable) FUIArrayTestDelegate *arrayDelegate; +@property (nonatomic, nullable) FUITestObservable *observable; +@property (nonatomic, nullable) FUISortedArray *array; +@property (nonatomic, nullable) FUIFakeSnapshot *snap; + +@end + +@implementation FUISortedArrayTest + +- (void)setUp { + [super setUp]; + self.arrayDelegate = [[FUIArrayTestDelegate alloc] init]; + self.snap = [[FUIFakeSnapshot alloc] init]; + self.observable = [[FUITestObservable alloc] init]; + self.array = [[FUISortedArray alloc] initWithQuery:self.observable + delegate:self.arrayDelegate + sortDescriptor:^NSComparisonResult(FIRDataSnapshot *left, + FIRDataSnapshot *right) { + return [left.key compare:right.key]; + }]; + [self.array observeQuery]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testArrayCanBeInitialized { + XCTAssertNotNil(self.array, @"expected array to not be nil when initialized"); +} + +- (void)testEmptyArrayUpdatesCountOnInsert { + // Test delegate + __block BOOL delegateWasCalled = NO; + __block BOOL expectedParametersWereCorrect = NO; + self.arrayDelegate.didAddObject = ^(FUISortedArray *array, id object, NSUInteger index) { + // Xcode complains about retain cycles if an XCTAssert is placed in here. + delegateWasCalled = YES; + expectedParametersWereCorrect = (array == self.array && + object == self.snap && + index == 0); + }; + + // Test insert + self.snap.key = @"snapshot"; + [self.observable sendEvent:FIRDataEventTypeChildAdded + withObject:self.snap + previousKey:nil + error:nil]; + // Array expectations + XCTAssert(self.array.count == 1, @"expected empty array to contain one item after insert"); + + // Delegate expectations + XCTAssert(delegateWasCalled, @"expected delegate to receive callback for insertion"); + XCTAssert(expectedParametersWereCorrect, @"unexpected parameter in delegate callback"); +} + +- (void)testItSortsItselfOnMiddleInsert { + [self.observable populateWithCount:10]; + + // Test delegate + __block BOOL delegateWasCalled = NO; + __block BOOL expectedParametersWereCorrect = NO; + self.arrayDelegate.didAddObject = ^(FUISortedArray *array, id object, NSUInteger index) { + // Xcode complains about retain cycles if an XCTAssert is placed in here. + delegateWasCalled = YES; + expectedParametersWereCorrect = (array == self.array && + object == self.snap && + // index should be 2 since "11" comes before "2" alphabetically. + index == 2); + }; + + // Test insert + self.snap.key = @"11"; + [self.observable sendEvent:FIRDataEventTypeChildAdded + withObject:self.snap + previousKey:@"0" // insert after "0" should be ignored to maintain sort + error:nil]; + // Array expectations + XCTAssert(self.array.count == 11, @"expected empty array to contain one item after insert"); + + // Delegate expectations + XCTAssert(delegateWasCalled, @"expected delegate to receive callback for insertion"); + XCTAssert(expectedParametersWereCorrect, @"unexpected parameter in delegate callback"); +} + +- (void)testItSortsItselfOnBeginningInsert { + [self.observable populateWithCount:10]; + + // Test delegate + __block BOOL delegateWasCalled = NO; + __block BOOL expectedParametersWereCorrect = NO; + self.arrayDelegate.didAddObject = ^(FUISortedArray *array, id object, NSUInteger index) { + // Xcode complains about retain cycles if an XCTAssert is placed in here. + delegateWasCalled = YES; + expectedParametersWereCorrect = (array == self.array && + object == self.snap && + // index should be 0 since "+" comes before "0" alphabetically. + index == 0); + }; + + // Test insert + self.snap.key = @"+"; + [self.observable sendEvent:FIRDataEventTypeChildAdded + withObject:self.snap + previousKey:@"0" // insert after "0" should be ignored to maintain sort + error:nil]; + // Array expectations + XCTAssert(self.array.count == 11, @"expected empty array to contain one item after insert"); + + // Delegate expectations + XCTAssert(delegateWasCalled, @"expected delegate to receive callback for insertion"); + XCTAssert(expectedParametersWereCorrect, @"unexpected parameter in delegate callback"); +} + +- (void)testItSortsItselfOnEndInsert { + [self.observable populateWithCount:10]; + + // Test delegate + __block BOOL delegateWasCalled = NO; + __block BOOL expectedParametersWereCorrect = NO; + self.arrayDelegate.didAddObject = ^(FUISortedArray *array, id object, NSUInteger index) { + // Xcode complains about retain cycles if an XCTAssert is placed in here. + delegateWasCalled = YES; + expectedParametersWereCorrect = (array == self.array && + object == self.snap && + // index should be 10 since "a" comes after "9" alphabetically. + index == 10); + }; + + // Test insert + self.snap.key = @"a"; + [self.observable sendEvent:FIRDataEventTypeChildAdded + withObject:self.snap + previousKey:@"0" // insert after "0" should be ignored to maintain sort + error:nil]; + // Array expectations + XCTAssert(self.array.count == 11, @"expected empty array to contain one item after insert"); + + // Delegate expectations + XCTAssert(delegateWasCalled, @"expected delegate to receive callback for insertion"); + XCTAssert(expectedParametersWereCorrect, @"unexpected parameter in delegate callback"); +} + +- (void)testItSortsItselfWhenChangingObjects { + [self.observable removeAllObservers]; + self.array = [[FUISortedArray alloc] initWithQuery:self.observable + delegate:self.arrayDelegate + sortDescriptor:^NSComparisonResult(FIRDataSnapshot *left, + FIRDataSnapshot *right) { + // sort by value, so that changes can cause moves as well + return [left.value compare:right.value]; + }]; + [self.array observeQuery]; + [self.observable populateWithCount:10]; + + // Test delegate. Changes in the sorted array are modelled as + // a remove and an insert, since changes may cause reordering. + __block BOOL removeWasCalled = NO; + __block BOOL insertWasCalled = NO; + __block BOOL removeParametersWereCorrect = NO; + __block BOOL insertParametersWereCorrect = NO; + self.arrayDelegate.didAddObject = ^(FUISortedArray *array, id object, NSUInteger index) { + // Xcode complains about retain cycles if an XCTAssert is placed in here. + insertWasCalled = YES; + insertParametersWereCorrect = (array == self.array && + object == self.snap && + index == 9); + NSLog(@"insert: %@", insertParametersWereCorrect ? @"YES" : @"NO"); + }; + self.arrayDelegate.didRemoveObject = ^(FUISortedArray *array, id object, NSUInteger index) { + removeWasCalled = YES; + removeParametersWereCorrect = (array == self.array && + index == 2); + NSLog(@"remove: %@", removeParametersWereCorrect ? @"YES" : @"NO"); + }; + + // Test change + self.snap.key = @"2"; + self.snap.value = @"a"; + [self.observable sendEvent:FIRDataEventTypeChildChanged + withObject:self.snap + previousKey:@"1" + error:nil]; + + // Delegate expectations + XCTAssert(removeWasCalled, @"expected delegate to receive callback for removal"); + XCTAssert(insertWasCalled, @"expected delegate to receive callback for insertion"); + XCTAssert(insertParametersWereCorrect, @"unexpected parameter in delegate callback"); + XCTAssert(removeParametersWereCorrect, @"unexpected parameter in delegate callback"); +} + +@end diff --git a/FirebaseUI.xcodeproj/project.pbxproj b/FirebaseUI.xcodeproj/project.pbxproj index c33cc782bde..9f72723d725 100644 --- a/FirebaseUI.xcodeproj/project.pbxproj +++ b/FirebaseUI.xcodeproj/project.pbxproj @@ -10,6 +10,11 @@ 8D01FC7B1DAEC2FF00BD7848 /* FUIIndexCollectionViewDataSourceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D01FC7A1DAEC2FF00BD7848 /* FUIIndexCollectionViewDataSourceTest.m */; }; 8D1E107C1DAEB97300AEFCA0 /* FUIIndexTableViewDataSourceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D1E107B1DAEB97300AEFCA0 /* FUIIndexTableViewDataSourceTest.m */; }; 8D2A84AA1D678B2B0058DF04 /* FirebaseUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D2A84A91D678B2B0058DF04 /* FirebaseUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8D306BBF1E1C64E200A13B0E /* FUIArrayTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941E91D67951B00CD3685 /* FUIArrayTest.m */; }; + 8D306BC01E1C64F000A13B0E /* FUICollectionViewDataSourceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941EC1D67951B00CD3685 /* FUICollectionViewDataSourceTest.m */; }; + 8D306BC11E1C64F200A13B0E /* FUITableViewDataSourceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941ED1D67951B00CD3685 /* FUITableViewDataSourceTest.m */; }; + 8D3A120E1DC2B122007558BA /* FUISortedArray.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D3A120C1DC2B122007558BA /* FUISortedArray.h */; }; + 8D3A120F1DC2B122007558BA /* FUISortedArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D3A120D1DC2B122007558BA /* FUISortedArray.m */; }; 8D78AF061D9D8CB000CFA9C5 /* UIImageView+FirebaseStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D7AD9B61D9317FB006866B9 /* UIImageView+FirebaseStorage.m */; }; 8D7AD9B71D9317FB006866B9 /* UIImageView+FirebaseStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D7AD9B51D9317FB006866B9 /* UIImageView+FirebaseStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8D7AD9B81D9317FB006866B9 /* UIImageView+FirebaseStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D7AD9B61D9317FB006866B9 /* UIImageView+FirebaseStorage.m */; }; @@ -26,7 +31,7 @@ 8D924C651DA6F77700C4DA48 /* FUIQueryObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D924C631DA6F77700C4DA48 /* FUIQueryObserver.m */; }; 8D924C661DA6F77700C4DA48 /* FUIQueryObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D924C631DA6F77700C4DA48 /* FUIQueryObserver.m */; }; 8DA9413E1D678DEB00CD3685 /* FUIArray.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA941391D678DEB00CD3685 /* FUIArray.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8DA9413F1D678DEB00CD3685 /* FUIArrayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA9413A1D678DEB00CD3685 /* FUIArrayDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DA9413F1D678DEB00CD3685 /* FUICollection.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA9413A1D678DEB00CD3685 /* FUICollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8DA941401D678DEB00CD3685 /* FUICollectionViewDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA9413B1D678DEB00CD3685 /* FUICollectionViewDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8DA941411D678DEB00CD3685 /* FUIDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA9413C1D678DEB00CD3685 /* FUIDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8DA941421D678DEB00CD3685 /* FUITableViewDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA9413D1D678DEB00CD3685 /* FUITableViewDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -95,10 +100,8 @@ 8DA941DF1D6793EE00CD3685 /* ic_facebook@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8DA941DC1D6793EE00CD3685 /* ic_facebook@3x.png */; }; 8DA941E31D67940100CD3685 /* FirebaseFacebookAuthUI.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8DA941E11D67940100CD3685 /* FirebaseFacebookAuthUI.strings */; }; 8DA941E81D67945100CD3685 /* FirebaseAuthUI.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8DA941E61D67945100CD3685 /* FirebaseAuthUI.strings */; }; - 8DA941EE1D67951B00CD3685 /* FUIArrayTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941E91D67951B00CD3685 /* FUIArrayTest.m */; }; - 8DA941EF1D67951B00CD3685 /* FUIDatabaseTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941EB1D67951B00CD3685 /* FUIDatabaseTestUtils.m */; }; - 8DA941F01D67951B00CD3685 /* FUICollectionViewDataSourceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941EC1D67951B00CD3685 /* FUICollectionViewDataSourceTest.m */; }; - 8DA941F11D67951B00CD3685 /* FUITableViewDataSourceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941ED1D67951B00CD3685 /* FUITableViewDataSourceTest.m */; }; + 8DAC55A81DD666CB00D52664 /* FUISortedArrayTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D60505E1DCA8EB8005D8635 /* FUISortedArrayTest.m */; }; + 8DAC55A91DD666D700D52664 /* FUIDatabaseTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941EB1D67951B00CD3685 /* FUIDatabaseTestUtils.m */; }; 8DAF8BF61D89CC5300B251C7 /* FUIAuthTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DAF8BF41D89CC5300B251C7 /* FUIAuthTest.m */; }; 8DAF8BF71D89CC5300B251C7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8DAF8BF51D89CC5300B251C7 /* GoogleService-Info.plist */; }; 8DBA0F7F1D872E4300D113D3 /* FUIArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DA941431D678E3200CD3685 /* FUIArray.m */; }; @@ -354,6 +357,9 @@ 8D2A84A61D678B2B0058DF04 /* FirebaseUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FirebaseUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8D2A84A91D678B2B0058DF04 /* FirebaseUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FirebaseUI.h; sourceTree = ""; }; 8D2A84AB1D678B2B0058DF04 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8D3A120C1DC2B122007558BA /* FUISortedArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUISortedArray.h; sourceTree = ""; }; + 8D3A120D1DC2B122007558BA /* FUISortedArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FUISortedArray.m; sourceTree = ""; }; + 8D60505E1DCA8EB8005D8635 /* FUISortedArrayTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FUISortedArrayTest.m; sourceTree = ""; }; 8D7AD9B51D9317FB006866B9 /* UIImageView+FirebaseStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImageView+FirebaseStorage.h"; sourceTree = ""; }; 8D7AD9B61D9317FB006866B9 /* UIImageView+FirebaseStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImageView+FirebaseStorage.m"; sourceTree = ""; }; 8D9084201D93168800333CC8 /* libStorage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libStorage.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -369,7 +375,7 @@ 8D924C621DA6F77700C4DA48 /* FUIQueryObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUIQueryObserver.h; sourceTree = ""; }; 8D924C631DA6F77700C4DA48 /* FUIQueryObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FUIQueryObserver.m; sourceTree = ""; }; 8DA941391D678DEB00CD3685 /* FUIArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUIArray.h; sourceTree = ""; }; - 8DA9413A1D678DEB00CD3685 /* FUIArrayDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUIArrayDelegate.h; sourceTree = ""; }; + 8DA9413A1D678DEB00CD3685 /* FUICollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUICollection.h; sourceTree = ""; }; 8DA9413B1D678DEB00CD3685 /* FUICollectionViewDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUICollectionViewDataSource.h; sourceTree = ""; }; 8DA9413C1D678DEB00CD3685 /* FUIDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUIDataSource.h; sourceTree = ""; }; 8DA9413D1D678DEB00CD3685 /* FUITableViewDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUITableViewDataSource.h; sourceTree = ""; }; @@ -844,9 +850,11 @@ 8DFA90411DAC1DAC003B5E10 /* FUIIndexTableViewDataSource.m */, 8DFA90451DAC54DD003B5E10 /* FUIIndexCollectionViewDataSource.h */, 8DFA90461DAC54DD003B5E10 /* FUIIndexCollectionViewDataSource.m */, + 8DA9413A1D678DEB00CD3685 /* FUICollection.h */, 8DA941391D678DEB00CD3685 /* FUIArray.h */, - 8DA9413A1D678DEB00CD3685 /* FUIArrayDelegate.h */, 8DA941431D678E3200CD3685 /* FUIArray.m */, + 8D3A120C1DC2B122007558BA /* FUISortedArray.h */, + 8D3A120D1DC2B122007558BA /* FUISortedArray.m */, 8DA9413B1D678DEB00CD3685 /* FUICollectionViewDataSource.h */, 8DA941441D678E3200CD3685 /* FUICollectionViewDataSource.m */, 8DA9413C1D678DEB00CD3685 /* FUIDataSource.h */, @@ -867,6 +875,7 @@ 8D1E107B1DAEB97300AEFCA0 /* FUIIndexTableViewDataSourceTest.m */, 8D01FC7A1DAEC2FF00BD7848 /* FUIIndexCollectionViewDataSourceTest.m */, 8DA941E91D67951B00CD3685 /* FUIArrayTest.m */, + 8D60505E1DCA8EB8005D8635 /* FUISortedArrayTest.m */, 8DA941EC1D67951B00CD3685 /* FUICollectionViewDataSourceTest.m */, 8DA941ED1D67951B00CD3685 /* FUITableViewDataSourceTest.m */, ); @@ -1098,10 +1107,11 @@ 8DFA90421DAC1DAC003B5E10 /* FUIIndexTableViewDataSource.h in Headers */, 8D924C641DA6F77700C4DA48 /* FUIQueryObserver.h in Headers */, 8DFA90471DAC54DD003B5E10 /* FUIIndexCollectionViewDataSource.h in Headers */, + 8D3A120E1DC2B122007558BA /* FUISortedArray.h in Headers */, 8DA9413E1D678DEB00CD3685 /* FUIArray.h in Headers */, 8DA941401D678DEB00CD3685 /* FUICollectionViewDataSource.h in Headers */, 8DA941421D678DEB00CD3685 /* FUITableViewDataSource.h in Headers */, - 8DA9413F1D678DEB00CD3685 /* FUIArrayDelegate.h in Headers */, + 8DA9413F1D678DEB00CD3685 /* FUICollection.h in Headers */, 8DA941411D678DEB00CD3685 /* FUIDataSource.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1900,6 +1910,7 @@ 8DFA90431DAC1DAC003B5E10 /* FUIIndexTableViewDataSource.m in Sources */, 8DFA90481DAC54DD003B5E10 /* FUIIndexCollectionViewDataSource.m in Sources */, 8D924C651DA6F77700C4DA48 /* FUIQueryObserver.m in Sources */, + 8D3A120F1DC2B122007558BA /* FUISortedArray.m in Sources */, 8DA941481D678E3200CD3685 /* FUICollectionViewDataSource.m in Sources */, 8DA941471D678E3200CD3685 /* FUIArray.m in Sources */, 8DA941491D678E3200CD3685 /* FUIDataSource.m in Sources */, @@ -1911,13 +1922,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8D306BC01E1C64F000A13B0E /* FUICollectionViewDataSourceTest.m in Sources */, + 8D306BC11E1C64F200A13B0E /* FUITableViewDataSourceTest.m in Sources */, + 8D306BBF1E1C64E200A13B0E /* FUIArrayTest.m in Sources */, 8D01FC7B1DAEC2FF00BD7848 /* FUIIndexCollectionViewDataSourceTest.m in Sources */, 8D1E107C1DAEB97300AEFCA0 /* FUIIndexTableViewDataSourceTest.m in Sources */, 8D924C611DA6F69100C4DA48 /* FUIIndexArrayTest.m in Sources */, - 8DA941F01D67951B00CD3685 /* FUICollectionViewDataSourceTest.m in Sources */, - 8DA941F11D67951B00CD3685 /* FUITableViewDataSourceTest.m in Sources */, - 8DA941EF1D67951B00CD3685 /* FUIDatabaseTestUtils.m in Sources */, - 8DA941EE1D67951B00CD3685 /* FUIArrayTest.m in Sources */, + 8DAC55A91DD666D700D52664 /* FUIDatabaseTestUtils.m in Sources */, + 8DAC55A81DD666CB00D52664 /* FUISortedArrayTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2414,14 +2426,11 @@ CLANG_WARN_SUSPICIOUS_MOVES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "\"$PODS_CONFIGURATION_BUILD_DIR/SDWebImage\"", "\"${PODS_ROOT}/FirebaseAnalytics/Frameworks/frameworks\"", "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks\"", "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks\"", - "\"${PODS_ROOT}/GoogleUtilities/Frameworks\"", "\"${PODS_ROOT}/FirebaseStorage/Frameworks/frameworks\"", - "\"${PODS_ROOT}/GoogleNetworkingUtilities/Frameworks\"", ); INFOPLIST_FILE = FirebaseStorageUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -2439,6 +2448,14 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/FirebaseAnalytics/Frameworks/frameworks\"", + "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", + "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks\"", + "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks\"", + "\"${PODS_ROOT}/FirebaseStorage/Frameworks/frameworks\"", + ); INFOPLIST_FILE = FirebaseStorageUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(BUILT_PRODUCTS_DIR)"; @@ -2610,7 +2627,6 @@ "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks\"", "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks\"", - "\"${PODS_ROOT}/GoogleUtilities/Frameworks\"", ); INFOPLIST_FILE = FirebaseDatabaseUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -2632,7 +2648,6 @@ "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks\"", "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks\"", - "\"${PODS_ROOT}/GoogleUtilities/Frameworks\"", ); INFOPLIST_FILE = FirebaseDatabaseUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -2699,8 +2714,6 @@ "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks/frameworks\"", - "\"${PODS_ROOT}/GoogleUtilities/Frameworks/frameworks\"", - "\"${PODS_ROOT}/GoogleNetworkingUtilities/Frameworks/frameworks\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", ); INFOPLIST_FILE = FirebaseAuthUITests/Info.plist; @@ -2722,8 +2735,6 @@ "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks/frameworks\"", - "\"${PODS_ROOT}/GoogleUtilities/Frameworks/frameworks\"", - "\"${PODS_ROOT}/GoogleNetworkingUtilities/Frameworks/frameworks\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", ); INFOPLIST_FILE = FirebaseAuthUITests/Info.plist; @@ -2745,9 +2756,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "\"$PODS_CONFIGURATION_BUILD_DIR/Bolts\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKCoreKit\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKLoginKit\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/GTMOAuth2-framework\"", ); @@ -2777,9 +2785,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "\"$PODS_CONFIGURATION_BUILD_DIR/Bolts\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKCoreKit\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKLoginKit\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/GTMOAuth2-framework\"", ); @@ -2804,15 +2809,10 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "\"$PODS_CONFIGURATION_BUILD_DIR/Bolts\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKCoreKit\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKLoginKit\"", "\"${PODS_ROOT}/FirebaseAnalytics/Frameworks/frameworks\"", "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks\"", "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks\"", - "\"${PODS_ROOT}/GoogleUtilities/Frameworks\"", - "\"${PODS_ROOT}/GoogleNetworkingUtilities/Frameworks\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", ); INFOPLIST_FILE = FirebaseFacebookAuthUITests/Info.plist; @@ -2829,15 +2829,10 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "\"$PODS_CONFIGURATION_BUILD_DIR/Bolts\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKCoreKit\"", - "\"$PODS_CONFIGURATION_BUILD_DIR/FBSDKLoginKit\"", "\"${PODS_ROOT}/FirebaseAnalytics/Frameworks/frameworks\"", "\"${PODS_ROOT}/FirebaseInstanceID/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleInterchangeUtilities/Frameworks\"", "\"${PODS_ROOT}/GoogleSymbolUtilities/Frameworks\"", - "\"${PODS_ROOT}/GoogleUtilities/Frameworks\"", - "\"${PODS_ROOT}/GoogleNetworkingUtilities/Frameworks\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", ); INFOPLIST_FILE = FirebaseFacebookAuthUITests/Info.plist; @@ -2918,7 +2913,6 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/build/Debug-iphoneos", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleSignIn/Frameworks\"", ); @@ -2942,7 +2936,6 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/build/Debug-iphoneos", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", "\"${PODS_ROOT}/GoogleSignIn/Frameworks\"", ); @@ -3026,9 +3019,7 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "\"$PODS_CONFIGURATION_BUILD_DIR/OCMock\"", "\"${PODS_ROOT}/TwitterKit/iOS\"", - "\"${PODS_ROOT}/TwitterCore/iOS\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", ); INFOPLIST_FILE = FirebaseTwitterAuthUITests/Info.plist; @@ -3045,9 +3036,7 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "\"$PODS_CONFIGURATION_BUILD_DIR/OCMock\"", "\"${PODS_ROOT}/TwitterKit/iOS\"", - "\"${PODS_ROOT}/TwitterCore/iOS\"", "\"${PODS_ROOT}/FirebaseAuth/Frameworks/frameworks\"", ); INFOPLIST_FILE = FirebaseTwitterAuthUITests/Info.plist; diff --git a/FirebaseUISample/FUIViewController.h b/FirebaseUISample/FUIViewController.h index dff11e9720e..5906641d30c 100644 --- a/FirebaseUISample/FUIViewController.h +++ b/FirebaseUISample/FUIViewController.h @@ -18,6 +18,4 @@ @interface FUIViewController : UITableViewController - @end - diff --git a/samples/objc/FirebaseUI-demo-objc/FUISamplesViewController.m b/samples/objc/FirebaseUI-demo-objc/FUISamplesViewController.m index 16da9b4e84c..d519f5e89a3 100644 --- a/samples/objc/FirebaseUI-demo-objc/FUISamplesViewController.m +++ b/samples/objc/FirebaseUI-demo-objc/FUISamplesViewController.m @@ -50,7 +50,7 @@ - (void)populateSamples { }]]; [samples addObject:[FUISample sampleWithTitle:@"Chat" - sampleDescription:@"Demonstrates using a FUICollectionViewDataSource to load data from Firebase Database into a UICollectionView for a basic chat app." + sampleDescription:@"Demonstrates using a FUICollectionViewDataSource to load data from Firebase Database into a UICollectionView for a basic chat app." controller:^UIViewController *{ UIViewController *controller = [[UIStoryboard storyboardWithName:@"Main" @@ -93,7 +93,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { FUISample *sample = _samplesContainer[indexPath.row]; UIViewController *viewController = sample.controllerBlock(); - + [self.navigationController pushViewController:viewController animated:YES]; }