From 4cf3b1e1df888c2f3f7253eaeebb84a32c5a7d8c Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 3 Jan 2017 15:12:27 -0800 Subject: [PATCH] add support for batching updates --- FirebaseDatabaseUI/FUIArray.h | 3 +- FirebaseDatabaseUI/FUIArray.m | 65 +++++++++++++++---- FirebaseDatabaseUI/FUICollection.h | 10 +++ FirebaseDatabaseUITests/FUIArrayTest.m | 33 ++++++++++ .../FUIDatabaseTestUtils.h | 2 + .../FUIDatabaseTestUtils.m | 12 ++++ FirebaseUI.xcodeproj/project.pbxproj | 6 ++ 7 files changed, 117 insertions(+), 14 deletions(-) diff --git a/FirebaseDatabaseUI/FUIArray.h b/FirebaseDatabaseUI/FUIArray.h index 5a560e2d260..79fa9576a68 100644 --- a/FirebaseDatabaseUI/FUIArray.h +++ b/FirebaseDatabaseUI/FUIArray.h @@ -43,7 +43,8 @@ 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 diff --git a/FirebaseDatabaseUI/FUIArray.m b/FirebaseDatabaseUI/FUIArray.m index 9fbead35a75..19fe039ce42 100755 --- a/FirebaseDatabaseUI/FUIArray.m +++ b/FirebaseDatabaseUI/FUIArray.m @@ -33,6 +33,14 @@ @interface FUIArray () */ @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 @implementation FUIArray @@ -68,53 +76,84 @@ - (void)dealloc { #pragma mark - Private API methods - (void)observeQuery { - if (self.handles.count == 4) { /* don't duplicate observers */ return; } + if (self.handles.count == 5) { /* don't duplicate observers */ return; } FIRDatabaseHandle handle; handle = [self.query observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousChildKey) { + [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) { + [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) { + [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) { + [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]; diff --git a/FirebaseDatabaseUI/FUICollection.h b/FirebaseDatabaseUI/FUICollection.h index 6f3129e8cbe..1f5b7655595 100644 --- a/FirebaseDatabaseUI/FUICollection.h +++ b/FirebaseDatabaseUI/FUICollection.h @@ -68,6 +68,16 @@ NS_ASSUME_NONNULL_BEGIN @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 an diff --git a/FirebaseDatabaseUITests/FUIArrayTest.m b/FirebaseDatabaseUITests/FUIArrayTest.m index 51238baa6d4..93fa1d51c0b 100644 --- a/FirebaseDatabaseUITests/FUIArrayTest.m +++ b/FirebaseDatabaseUITests/FUIArrayTest.m @@ -443,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 39f2231628a..510ab909ce0 100644 --- a/FirebaseDatabaseUITests/FUIDatabaseTestUtils.h +++ b/FirebaseDatabaseUITests/FUIDatabaseTestUtils.h @@ -92,6 +92,8 @@ NS_ASSUME_NONNULL_BEGIN @end @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); diff --git a/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m b/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m index b082ae1df02..a3c9cd75673 100644 --- a/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m +++ b/FirebaseDatabaseUITests/FUIDatabaseTestUtils.m @@ -218,6 +218,18 @@ - (void)populateWithCount:(NSUInteger)count { @implementation FUIArrayTestDelegate +- (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); diff --git a/FirebaseUI.xcodeproj/project.pbxproj b/FirebaseUI.xcodeproj/project.pbxproj index 22e8cda608f..9f72723d725 100644 --- a/FirebaseUI.xcodeproj/project.pbxproj +++ b/FirebaseUI.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 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 */; }; @@ -1919,6 +1922,9 @@ 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 */,