diff --git a/GeoFire/API/GFQuery.h b/GeoFire/API/GFQuery.h index b054359..9b9c00f 100644 --- a/GeoFire/API/GFQuery.h +++ b/GeoFire/API/GFQuery.h @@ -28,6 +28,7 @@ #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -42,6 +43,7 @@ typedef NS_ENUM(NSUInteger, GFEventType) { }; typedef void (^GFQueryResultBlock) (NSString *key, CLLocation *location); +typedef void (^GFQueryResultSnapshotBlock) (NSString *key, FIRDataSnapshot *snapshot); typedef void (^GFReadyBlock) (void); /** @@ -78,6 +80,33 @@ typedef void (^GFReadyBlock) (void); - (FirebaseHandle)observeEventType:(GFEventType)eventType withBlock:(GFQueryResultBlock)block; +/*! + Adds a snapshot observer for an event type. + + If you are storing model data and geo data in the same database location, + you may want access to the FIRDataSnapshot as part of geo events. + In this case, use a snapshot observers rather than a key observers. + These snapshot observers have all of the same events as the key observers. + + The following event types are supported: + + typedef NS_ENUM(NSUInteger, GFEventType) { + GFEventTypeKeyEntered, // A key entered the search area + GFEventTypeKeyExited, // A key exited the search area + GFEventTypeKeyMoved // A key moved within the search area + }; + + The block is called for each event and snapshot. + + Use removeObserverWithFirebaseHandle: to stop receiving callbacks. + + @param eventType The event type to receive updates for + @param block The block that is called for updates + @return A handle to remove the observer with + */ + +- (FirebaseHandle)observeEventType:(GFEventType)eventType withSnapshotBlock:(GFQueryResultSnapshotBlock)block; + /** * Adds an observer that is called once all initial GeoFire data has been loaded and the relevant events have * been fired for this query. Every time the query criteria is updated, this observer will be called after the diff --git a/GeoFire/Implementation/GFQuery.m b/GeoFire/Implementation/GFQuery.m index f5c1c51..66bdda3 100644 --- a/GeoFire/Implementation/GFQuery.m +++ b/GeoFire/Implementation/GFQuery.m @@ -21,6 +21,7 @@ @interface GFQueryLocationInfo : NSObject @property (nonatomic) BOOL isInQuery; @property (nonatomic, strong) CLLocation *location; @property (nonatomic, strong) GFGeoHash *geoHash; +@property (nonatomic, strong) FIRDataSnapshot *snapshot; @end @@ -178,6 +179,16 @@ @interface GFQuery () @property (nonatomic, strong) NSMutableDictionary *keyEnteredObservers; @property (nonatomic, strong) NSMutableDictionary *keyExitedObservers; @property (nonatomic, strong) NSMutableDictionary *keyMovedObservers; + +/** + * The group of snapshot observers initialized by calling + * @see GFQuery::observeEventType:withSnapshotBlock: + * Useful when you are storing model data and geo data in the same database location. + */ +@property (nonatomic, strong) NSMutableDictionary *keyEnteredSnapshotObservers; +@property (nonatomic, strong) NSMutableDictionary *keyExitedSnapshotObservers; +@property (nonatomic, strong) NSMutableDictionary *keyMovedSnapshotObservers; + @property (nonatomic, strong) NSMutableDictionary *readyObservers; @property (nonatomic) NSUInteger currentHandle; @@ -204,6 +215,7 @@ - (FIRDatabaseQuery *)firebaseForGeoHashQuery:(GFGeoHashQuery *)query - (void)updateLocationInfo:(CLLocation *)location forKey:(NSString *)key + snapshot:(FIRDataSnapshot *)snapshot { NSAssert(location != nil, @"Internal Error! Location must not be nil!"); GFQueryLocationInfo *info = self.locationInfos[key]; @@ -220,6 +232,7 @@ - (void)updateLocationInfo:(CLLocation *)location info.location = location; info.isInQuery = [self locationIsInQuery:location]; info.geoHash = [GFGeoHash newWithLocation:location.coordinate]; + info.snapshot = snapshot; if ((isNew || !wasInQuery) && info.isInQuery) { [self.keyEnteredObservers enumerateKeysAndObjectsUsingBlock:^(id observerKey, @@ -229,6 +242,13 @@ - (void)updateLocationInfo:(CLLocation *)location block(key, info.location); }); }]; + [self.keyEnteredSnapshotObservers enumerateKeysAndObjectsUsingBlock:^(id observerKey, + GFQueryResultSnapshotBlock block, + BOOL *stop) { + dispatch_async(self.geoFire.callbackQueue, ^{ + block(key, info.snapshot); + }); + }]; } else if (!isNew && changedLocation && info.isInQuery) { [self.keyMovedObservers enumerateKeysAndObjectsUsingBlock:^(id observerKey, GFQueryResultBlock block, @@ -237,6 +257,13 @@ - (void)updateLocationInfo:(CLLocation *)location block(key, info.location); }); }]; + [self.keyMovedSnapshotObservers enumerateKeysAndObjectsUsingBlock:^(id observerKey, + GFQueryResultSnapshotBlock block, + BOOL *stop) { + dispatch_async(self.geoFire.callbackQueue, ^{ + block(key, info.snapshot); + }); + }]; } else if (wasInQuery && !info.isInQuery) { [self.keyExitedObservers enumerateKeysAndObjectsUsingBlock:^(id observerKey, GFQueryResultBlock block, @@ -245,6 +272,13 @@ - (void)updateLocationInfo:(CLLocation *)location block(key, info.location); }); }]; + [self.keyExitedSnapshotObservers enumerateKeysAndObjectsUsingBlock:^(id observerKey, + GFQueryResultSnapshotBlock block, + BOOL *stop) { + dispatch_async(self.geoFire.callbackQueue, ^{ + block(key, info.snapshot); + }); + }]; } } @@ -263,7 +297,7 @@ - (void)childAdded:(FIRDataSnapshot *)snapshot @synchronized(self) { CLLocation *location = [GeoFire locationFromValue:snapshot.value]; if (location != nil) { - [self updateLocationInfo:location forKey:snapshot.key]; + [self updateLocationInfo:location forKey:snapshot.key snapshot:snapshot]; } else { // TODO: error? } @@ -275,7 +309,7 @@ - (void)childChanged:(FIRDataSnapshot *)snapshot @synchronized(self) { CLLocation *location = [GeoFire locationFromValue:snapshot.value]; if (location != nil) { - [self updateLocationInfo:location forKey:snapshot.key]; + [self updateLocationInfo:location forKey:snapshot.key snapshot:snapshot]; } else { // TODO: error? } @@ -307,6 +341,13 @@ - (void)childRemoved:(FIRDataSnapshot *)snapshot block(key, location); }); }]; + [self.keyExitedSnapshotObservers enumerateKeysAndObjectsUsingBlock:^(id observerKey, + GFQueryResultSnapshotBlock block, + BOOL *stop) { + dispatch_async(self.geoFire.callbackQueue, ^{ + block(key, snapshot); + }); + }]; } } } @@ -391,7 +432,7 @@ - (void)updateQueries }]; self.queries = newQueries; [self.locationInfos enumerateKeysAndObjectsUsingBlock:^(id key, GFQueryLocationInfo *info, BOOL *stop) { - [self updateLocationInfo:info.location forKey:key]; + [self updateLocationInfo:info.location forKey:key snapshot:info.snapshot]; }]; NSMutableArray *oldLocations = [NSMutableArray array]; [self.locationInfos enumerateKeysAndObjectsUsingBlock:^(id key, GFQueryLocationInfo *info, BOOL *stop) { @@ -423,6 +464,9 @@ - (void)reset self.keyEnteredObservers = [NSMutableDictionary dictionary]; self.keyExitedObservers = [NSMutableDictionary dictionary]; self.keyMovedObservers = [NSMutableDictionary dictionary]; + self.keyEnteredSnapshotObservers = [NSMutableDictionary dictionary]; + self.keyExitedSnapshotObservers = [NSMutableDictionary dictionary]; + self.keyMovedSnapshotObservers = [NSMutableDictionary dictionary]; self.readyObservers = [NSMutableDictionary dictionary]; self.locationInfos = [NSMutableDictionary dictionary]; } @@ -441,6 +485,9 @@ - (void)removeObserverWithFirebaseHandle:(FirebaseHandle)firebaseHandle [self.keyEnteredObservers removeObjectForKey:handle]; [self.keyExitedObservers removeObjectForKey:handle]; [self.keyMovedObservers removeObjectForKey:handle]; + [self.keyEnteredSnapshotObservers removeObjectForKey:handle]; + [self.keyExitedSnapshotObservers removeObjectForKey:handle]; + [self.keyMovedSnapshotObservers removeObjectForKey:handle]; [self.readyObservers removeObjectForKey:handle]; if ([self totalObserverCount] == 0) { [self reset]; @@ -450,7 +497,10 @@ - (void)removeObserverWithFirebaseHandle:(FirebaseHandle)firebaseHandle - (NSUInteger)totalObserverCount { - return (self.keyEnteredObservers.count + + return (self.keyEnteredSnapshotObservers.count + + self.keyExitedSnapshotObservers.count + + self.keyMovedSnapshotObservers.count + + self.keyEnteredObservers.count + self.keyExitedObservers.count + self.keyMovedObservers.count + self.readyObservers.count); @@ -506,6 +556,56 @@ - (FirebaseHandle)observeEventType:(GFEventType)eventType withBlock:(GFQueryResu } } +- (FirebaseHandle)observeEventType:(GFEventType)eventType withSnapshotBlock:(GFQueryResultSnapshotBlock)block +{ + @synchronized(self) { + if (block == nil) { + [NSException raise:NSInvalidArgumentException format:@"Block is not allowed to be nil!"]; + } + FirebaseHandle firebaseHandle = self.currentHandle++; + NSNumber *numberHandle = [NSNumber numberWithUnsignedInteger:firebaseHandle]; + switch (eventType) { + case GFEventTypeKeyEntered: { + [self.keyEnteredSnapshotObservers setObject:[block copy] + forKey:numberHandle]; + self.currentHandle++; + dispatch_async(self.geoFire.callbackQueue, ^{ + @synchronized(self) { + [self.locationInfos enumerateKeysAndObjectsUsingBlock:^(NSString *key, + GFQueryLocationInfo *info, + BOOL *stop) { + if (info.isInQuery) { + block(key, info.snapshot); + } + }]; + }; + }); + break; + } + case GFEventTypeKeyExited: { + [self.keyExitedSnapshotObservers setObject:[block copy] + forKey:numberHandle]; + self.currentHandle++; + break; + } + case GFEventTypeKeyMoved: { + [self.keyMovedSnapshotObservers setObject:[block copy] + forKey:numberHandle]; + self.currentHandle++; + break; + } + default: { + [NSException raise:NSInvalidArgumentException format:@"Event type was not a GFEventType!"]; + break; + } + } + if (self.queries == nil) { + [self updateQueries]; + } + return firebaseHandle; + } +} + - (FirebaseHandle)observeReadyWithBlock:(GFReadyBlock)block { @synchronized(self) { diff --git a/examples/SFVehicles/SFVehicles.xcodeproj/xcshareddata/xcschemes/SFVehicles.xcscheme b/examples/SFVehicles/SFVehicles.xcodeproj/xcshareddata/xcschemes/SFVehicles.xcscheme new file mode 100644 index 0000000..069dcc2 --- /dev/null +++ b/examples/SFVehicles/SFVehicles.xcodeproj/xcshareddata/xcschemes/SFVehicles.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +