Firebase PresenceManaging iOS - ios

I'm implementing a system based on firebase docs:
[connectionMonitor observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
if([snapshot.value boolValue]) {
// connection established (or I've reconnected after a loss of connection)
// add this device to my connections list
// this value could contain info about the device or a timestamp instead of just true
Firebase * con = [[Firebase alloc]initWithUrl:[NSString stringWithFormat:#"%#Users/%#/connections/", urlString, currentUserId]];
Firebase * newConnection = [con childByAutoId];
[newConnection setValue:#YES];
// when this device disconnects, remove it
[newConnection onDisconnectRemoveValue];
}
}];
Which works fine, if the user fully disconnects, but that's my problem.
I use this system to see if the user is online. If they're not online, I trigger a push notification. If the user closes the app, firebase doesn't disconnect, but it also doesn't receive updates, so on the other end, the user looks like they are still online. For the firebase onDisconnect value to properly set, the user is required to completely close out of the app.
I have resolved this by adding:
- (void)applicationWillResignActive:(UIApplication *)application
{
[Firebase goOffline];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[Firebase goOffline];
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
[Firebase goOnline];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[Firebase goOnline];
}
Is this normal behavior, or am I doing something wrong?

This is (currently) expected behavior. Firebase won't trigger the presence actions until the client actually disconnects and iOS will leave the underlying socket connection alive for some period of time (probably less than 5 minutes) after the app goes to the background... so presence will be delayed. It should still definitely happen eventually though.
Your workaround should work fine, or to avoid tearing down the whole connection, you could just set the presence bit to #NO / #YES on going to background / foreground.
I can see how most apps would expect presence to kick in when the app goes to the background, so we may investigate changing this behavior in the future.

Related

When you close the application, boolean not set to no

I use Firebase in the application. I want to record the user's status in the database, it is currently in the application or not. I use the code from the official document, which must ensure this task. When the application is opened, the value is set to NO, and then changed to YES. But at the close of the application as well as the transition to the background, the block is not called and the value does not change. Here is my code...
FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:#".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
if([snapshot.value boolValue]) {
NSLog(#"CONNECTED");
} else {
NSLog(#"NOT CONNECTED");
}
}];
2017-01-17 13:36:38.203 Tricker[6293:817528] NOT CONNECTED
2017-01-17 13:36:40.863 Tricker[6293:817528] CONNECTED
The console comes the following information, which shows that the value changes after two seconds after the opening of the application with NO to YES. But nothing happens when the application is closed...
Prompt in what could be the problem?
You seem to be confusing connection state and application lifecycle.
When it is active, the Firebase Database client will fire information about its connection state on the .info/connected path.
But when the app is not active, the client has no way of firing this information anymore. You will need to use regular iOS application lifecycle events to detect when the app becomes inactive.
Recording the user's presence in the database works as a two-step process:
when the application starts you write a value to the database to signal that the user is online
when the application starts, you tell the database server to write another value to the database that signals that the user is gone.
Step 2 is accomplished by onDisconnectSetValue:
[presenceRef onDisconnectSetValue:#"I disconnected!"];
The trick is that you call this method early on, typically when your app starts. The write operation will be executed when the Firebase Database server detects that the client disconnected.
This disconnect can happen in two ways:
when the client closes the connection explicitly
when the the socket that the server uses to communicate with the client times out
When your app crashes, you're in situation 2. In that case it can take a few minutes before the server detects that the client is gone, since you're waiting for a socket to time-out.
You close the app means ? two things possibility
User can put app in Background :
Here you can set a NSUSEDefault value like :
(void)applicationDidEnterBackground:(UIApplication *)application {
[[NSUserDefaults standardUserDefaults] setBool:NO
forKey:#"YOUR_KEY"]; [[NSUserDefaults standardUserDefaults]
synchronize];
}
User can remove app from background :
(void)applicationWillTerminate:(UIApplication *)application {
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:#"YOUR_KEY"];
[[NSUserDefaults standardUserDefaults] synchronize];
}

watchOS 2.0 - Can't cancel WCSessionFileTransfer

Our app lets a user select records on iPhone that they want to be displayed in the watch app.
It works like this:
The user taps "Add to watch" on a record from their iPhone
A new version of the watch database is generated and sent to the watch
The watch app receives and saves the file and updates its interface
A new database file is sent to the watch and processed for each change. This is fine if the watch is awake since it will give the user live updates, but if the watch is asleep while the user makes 7 changes, it means the watch is accepting and processing 7 new files as soon as it wakes up.
We really only care about the most recent version of the watch database, so I'm trying to cancel all old outstanding file transfers.
Code:
On iPhone, each time a record is added/removed from watch database, we attempt (unsuccessfully) to cancel pending file transfers and then queue the latest database file:
// create watch database and store it at self.urlToDatabase
[self generateNewWatchDatabase];
if ([WCSession isSupported])
{
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
// this is the problem - cancel doesn't seem to do anything
for (WCSessionFileTransfer *fileTransfer in session.outstandingFileTransfers)
[fileTransfer cancel];
[session transferFile:self.urlToDatabase metadata:nil];
}
In the above code, calling [fileTransfer cancel] successfully removes the WCSessionFileTransfer object from session.outstandingFileTransfers, but didReceiveFile is still being called multiple times below.
Accepting the file on the watch:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file
{
// this method gets called once for every time -transferFile:metadata: is called above,
// even after cancelling outstanding file transfers
[self replaceDatabaseWithFile:file];
[self refreshItemsTable];
}
How do we cancel outstanding file transfers?
Edit
As per #ccjensen's recommendation, I tried the following in the method that fires when the user adds/removes a record to/from the watch:
// store a reference to the file transfer and immediately cancel it
WCSessionFileTransfer *transfer = [session transferFile:self.urlToDatabase metadata:nil];
[transfer cancel];
This still results in the file being sent to the watch, instead of cancelling it as one would expect.
I also tried the following:
Kill watch app (by holding the side button until 'Power Off' appears, and then holding it again)
Add/remove records from iPhone
Relaunch watch app
Even in this scenario the watch receives all 'cancelled' file transfers.
The documentation for the cancel method says:
Use this method to cancel a file transfer before it completes. If the file has already been transferred, calling this method has no effect.
So it sounds like the cancels are "best effort" and might not end up being able to cancel them in all cases, especially if the file has already been transferred.
Are you seeing it never work, even if you call cancel immediately (try testing without the watch app running as that seems to expedite the transfers)?
turned out the reason this worked was that the file url did not match the transfer's url I was checking šŸ™ˆ
I recently found that keeping hold of the WCSessionFileTransfer in my own array and canceling them proved more reliable than using [WCSession defaultSession].outstandingFileTransfers.
NSMutableArray<WCSessionFileTransfer*>* inProgressTransfers = [NSMutableArray array];
So each time you call TransfeFile: metaData:
WCSessionFileTransfer* transfer = [[WCSession defaultSession] transferFile:url metadata:metadata];
if (transfer)
{
[self.inProgressTransfers addObject:transfer];
}
then at an appropriate time
for (WCSessionFileTransfer* ourTransfer in self.inProgressTransfers)
{
[ourTransfer cancel];
[self.inProgressTransfers removeObject:ourTransfer];
}
For some reason, keeping hold of the transfers ourself makes calling cancel work much more reliably.. hope that helps someone

Player in game stopped playing

I'm working on a game in iOS, which needs an internet connection. The problem I'm having is setting a Bool on the server (parse.com) when the player stops playing. Right now I'm doing this...
-(void)applicationDidEnterBackground:(UIApplication *)application
{
NSLog(#"Player logged out");
[self playerLoggedOut];
}
and
-(void)playerLoggedOut
{
PFUser *currentUser = [PFUser currentUser];
if (currentUser) {
[currentUser setObject:[NSNumber numberWithBool:NO] forKey:#"playing"];
[currentUser saveEventually];
[sharedInstance requestSentWithDesc:#"Player logged out"];
}
}
But that doesn't seem to be working, there's also the situation of the game crashing, when I believe the above would never get called.
There's also...
-(void)applicationWillTerminate:(UIApplication *)application
But I'm not sure when's that called.
There is the possibility of using a timer, but if the player never logs back in, or doesn't for a long time I'm not sure how a timer could be used.
For your current processing you would need to start a background task (see here) which starts before you make the request and ends in the completion block (so change the save method you use).
For the other cases this won't work and you may want to add some kind of 'keep-alive' message an / or parse cloud code which monitors the user interaction and automatically logs them out if they are inactive for a set time.
If your game is crashing, you must get any logs, for example:
Or something else. You can add NSLog(#"worked to the bitter end!"); at the end of your -(void)playerLoggedOut or requestSentWithDesc method.

Stop publishing when there are no subscribers and auto start when there are subscribers

How would I implement a RACSignal that would stop publishing when there are no subscribers to it and auto start when there are subscribers?
Here is a scenario:
Let us say I have a currentLocationSignal in the AppDelegate.
My LocationViewController would subscribe to the currentLocationSignal when view loads and unsubscribe (dispose) when view unloads. Since it takes few seconds to get the current location, I would like to always subscribe to the currentLocationSignal when the app opens (and auto unsubscribe after few seconds), so by the time I arrive to LocationViewController I would get an accurate location.
So there can be more then one subscribers to the signal. When the first subscriber listens, it needs to start calling startUpdatingLocation and when there are no subscribers it needs to call stopUpdatingLocation.
Good question! Normally, you'd use RACMulticastConnection for use cases like this, but, because you want the signal to be able to reactivate later, a connection isn't suitable on its own.
The simplest answer is probably to mimic how a connection works, but with the specific behaviors you want. Basically, we'll keep track of how many subscribers there are at any given time, and start/stop updating the location based on that number.
Let's start by adding a locationSubject property. The subject needs to be a RACReplaySubject, because we always want new subscribers to get the most recently sent location immediately. Implementing updates with that subject is easy enough:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
[self.locationSubject sendNext:locations.lastObject];
}
Then, we want to implement the signal that tracks and increments/decrements the subscriber count. This works by using a numberOfLocationSubscribers integer property:
- (RACSignal *)currentLocationSignal {
return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
#synchronized (self) {
if (self.numberOfLocationSubscribers == 0) {
[self.locationManager startUpdatingLocation];
}
++self.numberOfLocationSubscribers;
}
[self.locationSubject subscribe:subscriber];
return [RACDisposable disposableWithBlock:^{
#synchronized (self) {
--self.numberOfLocationSubscribers;
if (self.numberOfLocationSubscribers == 0) {
[self.locationManager stopUpdatingLocation];
}
}
}];
}];
}
In the above code, the +createSignal: block is invoked every time a new subscriber is added to the returned signal. When that happens:
We check to see if the number of subscribers is currently zero. If so, the just-added subscriber is the first one, so we need to enable (or re-enable) location updates.
We hook the subscriber directly up to our locationSubject, so the values from the latter are automatically fed into the former.
Then, at some future time, when the subscription is disposed of, we decrement the count and stop location updates if appropriate.
Now, all that's left is subscribing to the currentLocationSignal on startup, and automatically unsubscribing after a few seconds:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Use a capacity of 1 because we only ever care about the latest
// location.
self.locationSubject = [RACReplaySubject replaySubjectWithCapacity:1];
[[self.currentLocationSignal
takeUntil:[RACSignal interval:3]]
subscribeCompleted:^{
// We don't actually need to do anything here, but we need
// a subscription to keep the location updating going for the
// time specified.
}];
return YES;
}
This subscribes to self.currentLocationSignal immediately, and then automatically disposes of that subscription when the +interval: signal sends its first value.
Interestingly, -[RACMulticastConnection autoconnect] used to behave like -currentLocationSignal above, but that behavior was changed because it makes side effects wildly unpredictable. This use case should be safe, but there are other times (like when making a network request or running a shell command) when automatic reconnection would be horrible.

ios logins using NSUserDefaults, handle crash or device shutdown

Iā€™m trying to handle critical behavior when user login on my ipad application.
Once successfully logged in, we will remember the login using nsuserdefaults but will require a re-login when:
Crash occurred
Device was shutdown
is it possible to reset the login value on NSUserDefault when the above actions occurred ? if Yes how can I handle them ?
thanks
For case 1 add an exception handler
- (void) applicationDidFinishLaunching: (UIApplication *) application
{
NSSetUncaughtExceptionHandler (&myExceptionHandler);
}
....
- (void) myExceptionHandler (NSException *exception)
{
// Make a note in your default settings
// User needs to log in
}
For case 2 trap
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Make a note in your default settings
// User needs to log in
}
I don't know of a way for an app to know when the device is powered off. If you meant when the application is exited read on.
Or just stick the logic in the
-(void)applicationDidBecomeActive:(UIApplication *)application
And catch both scenarios.

Resources