Receiving most recent old PubNub message after unsubscribing and resubscribing to channel - ios

I just started using the PubNub iOS SDK v4.0 in my project.
The whole goal of using PubNub is a user will be subscribed to a certain channel based on what UIViewController they are currently on.
So the user should never be subscribed to or receiving messages from more than one channel at any given time.
Unfortunately I can't get this to work properly. Here's the example scenario that keeps happening to me:
I store and setup the client and configuration properties in the App Delegate like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Pubnub
self.configuration = [PNConfiguration configurationWithPublishKey:pubNubPublishKey
subscribeKey:pubNubSubscribeKey];
self.client = [PubNub clientWithConfiguration:self.configuration];
return YES;
}
User lands on View Controller A and I subscribe them to Channel_A:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// App Delegate
self.appDelegate = [[UIApplication sharedApplication] delegate];
// PubNub
[self.appDelegate.client addListener:self];
[self.appDelegate.client subscribeToChannels:#[#"Channel_A"] withPresence:NO];
}
User leaves View Controller A to go to View Controller B, so I unsubscribe them from Channel A:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Remove listener + unsubscribe
[self.appDelegate.client removeListener:self];
[self.appDelegate.client unsubscribeFromChannels:#[#"Channel_A"] withPresence:NO];
}
I then use the same code structure as above to subscribe the user to Channel_B once they segue to View Controller B.
I publish 2 new PubNub messages on View Controller B, one goes to Channel B and another one goes to Channel_A. The current user only receives the message for Channel_B, but any other users on the app currently on View Controller A will receive the Channel_A message.
This almost works perfectly. The current user on View Controller B only receives the Channel_B message, but here's where I run into problems.
When the user leaves View Controller B and goes back to View Controller A, they instantly receive the most recent message that was just posted moments ago from View Controller B for Channel_A.
I don't understand why they are receiving this message when they were unsubscribed from Channel_A while on View Controller B.
I have even tested it and waited a minute to pop back to View Controller A, and I still always receive the most recent Channel_A message that was posted a minute ago.
I don't want this to happen. They should only receive real time messages for that channel, not one that happened 10 seconds ago, 30 seconds ago, etc. when they were unsubscribed from that channel.
I did some quick research and thought setting the following properties in the App Delegate might help, but I still experience this problem:
self.configuration.restoreSubscription = NO;
self.configuration.catchUpOnSubscriptionRestore = NO;

Just figured it out. I had to add the following line of code for my configuration:
self.configuration.keepTimeTokenOnListChange = NO;
By default this is set to YES and will retrieve the most recent message when subscribing to a channel.
I also was able to stop setting catchUpOnSubscriptionRestore to NO but still had to keep setting restoreSubscription to NO.

Related

SKStoreReviewController how to detect that user has turned off Rate This App (RTA) in settings or 3 times limit has reached?

Starting iOS 10.3, Apple is limiting the review prompt (Rate This App) to 3 times a year and it can be turned off in the user's settings.
Q: How do we detect that the 3 times limit has reached or if the user has turned off RTA so in the app I won't show a popup saying: "Did you like the app? If yes, can you write a review? [Yes/No]" because then, if the user taps Yes, nothing will show up.
There is really not much information here from the official documentation: https://developer.apple.com/reference/storekit/skstorereviewcontroller
Although you should call this method when it makes sense in the user experience flow of your app, the actual display of a rating/review request view is governed by App Store policy. Because this method may or may not present an alert, it's not appropriate to call it in response to a button tap or other user action.
Preamble
Asking users if they like the app might lead to your app being rejected. Here is an example:
https://twitter.com/pietbrauer/status/791883047373246464
In case the link dies here is an excerpt of Apples response:
3.2.2 ... your app includes content and features that can manipulate the user reviews or chart rankings on the App Store. Specifically, your app filters user reviews and only directs users who intend to rate your app 4 - 5 stars to complete a rating on the App Store...
I personally believe that this can be a valid tactic if you genuinely try to resolve the users issue, and still give them an opportunity to leave a review afterwards, but the question remains if Apple will see it that way.
Possible solution
Show popup asking the user if they enjoy/like/etc using the app.
Try using [SKStoreReviewController requestReview] to get a review.
Check if the number of windows has changed, indicating that a popup has been shown. The caveat here is that this is not 100% reliable since some other event can cause the number of windows to change.
If the number of windows stays the same use deep linking to forward the user to the app store. The docs for SKStoreReviewController suggest using action=write-review as a query parameter to go directly to the reviews page.
Here is a simple implementation:
// make sure we the current iOS version supports in app reviews
if ([SKStoreReviewController class])
{
NSUInteger windowCount = [UIApplication sharedApplication].windows.count;
[SKStoreReviewController requestReview];
// give the review controller some time to display the popup
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
if (windowCount < [UIApplication sharedApplication].windows.count)
{
// assume review popup showed instead of some other system alert
// for example show "thank you"
}
else
{
// open app store to leave review
NSURL *reviewUrl = [NSURL URLWithString:#"{your-app-url}?action=write-review"];
[[UIApplication sharedApplication] openURL:reviewUrl];
}
});
}
Note: I have not submitted this code to the App Store, so this is only theoretical.
Well, you could try to fire the request and see but as long as there's no callback as well as no other official way how to detect whether the rating alert has been displayed at the time you call the requesting method.
There is a way around however – one of the StoreKit classes can be swizzled so you can observe when the Rating dialog is opened.
UIWindow-inspecting ways mentioned around may be useful as well, but swizzling on a method call is probably more reliable.
You can also use some rating managers like AppRating available as a pod, which manage the stuff for you, but only on a naive level by counting the calls and remembering it.
I am using this solution in production code - so far no rejections from Apple:
NSUInteger windowCount = [UIApplication sharedApplication].windows.count;
// SKStoreReviewController only available for >= 10.3, if needed check for class existence
[SKStoreReviewController requestReview];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
BOOL shown = windowCount < [UIApplication sharedApplication].windows.count;
if (shown) {
//popup was shown
}
};
Building on the previous answers to "sniff" the window count for changes, here's a version that's working in Swift 5.4, on iOS 10.3 through 14.4:
func currentWindowCount() -> Int { UIApplication.shared.windows.count }
let initialWindowCount = currentWindowCount()
if #available(iOS 14.0, *) {
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
}
} else {
SKStoreReviewController.requestReview()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let actuallyShowedPrompt = initialWindowCount < currentWindowCount()
if actuallyShowedPrompt {
// do stuff
}
}

Scheduled LocalNotification in iOS not seen in scheduledLocalNotifications straight away

The problem I am encountering is that I successfully schedule a local notification for between 1 and 2 hours from now, I then go back to my main root view controller where some code runs and displays a different box depending on whether a local notification is scheduled or not. So the code is:
/// Commit notification
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
[self performSegueWithIdentifier:#"exitToStart" sender:self];
And then on the main root controller a method is called straight away on return to refresh the view that contains:
NSArray *currentNotifications = [[UIApplication sharedApplication] scheduledLocalNotifications];
if ([currentNotifications count] > 0)
{
...
}
else { ... }
The problem I am having is that sometimes currentNotifications count is 0 and sometimes it is 1 and so the wrong box is displayed. I think what is happening is sometimes iOS is still scheduling the notification and sometimes it is not, assuming it does it in the background without blocking the thread?
I know the local notification always gets there because if I come in and out of root when the problem occurs then it always finds the local notification.
Is there a way I can wait for it to be scheduled before looking for notifications? That complies with Apple's guidelines. Within the if block I go on to use info from the notification.
I couldn't see a way to guarantee that by the time I was back in main controller, the notification would always be scheduled. So to get around this I pass back the information I put into the local notification myself to the main controller in its return method. Later returns to the main controller from other areas then have given the local notification the 1 or 2 seconds it needs to schedule itself and can use the existing logic.

NSNotificationCenter callback while app in background

One question and one issue:
I have the following code:
- (void) registerForLocalCalendarChanges
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(localCalendarStoreChanged) name:EKEventStoreChangedNotification object:store ];
}
- (void) localCalendarStoreChanged
{
// This gets call when an event in store changes
// you have to go through the calendar to look for changes
[self getCalendarEvents];
}
These methods are in a class/object called CalendarEventReporter which contains the method getCalendarEvents (in the callback).
Two things:
1) If the app is in the background the callback does not run. Is there a way to make it do that?
2) When I bring the app back into the foreground (after having changed the calendar on the device) the app crashes without any error message in the debug window or on the device. My guess is that the CalendarEventReporter object that contains the callback is being garbage-collected. Is that possible? Any other thoughts on what might be causing the crash? Or how to see any error messages?
1) In order for the app to run in the background you should be using one of the modes mentioned in the "Background Execution and Multitasking section here:
uses location services
records or plays audio
provides VOIP
services
background refresh
connection to external devices
like through BLE
If you are not using any of the above, it is not possible to get asynchronous events in the background.
2) In order to see the crash logs/call stack place an exception breakpoint or look into the "Device Logs" section here: Window->Organizer->Devices->"Device Name" on left->Device Logs on Xcode.
To answer your first question, take a look at https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html
What I did to get code running in the background is to do something like
In the .h file
UIBackgroundTaskIdentifier backgroundUploadTask;
In the .m file
-(void) functionYouWantToRunInTheBackground
{
self.backgroundUploadTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
//code to do something
}
-(void) endBackgroundUpdateTask
{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUploadTask];
self.backgroundUploadTask = UIBackgroundTaskInvalid;
}
The code above I pretty much learned from objective c - Proper use of beginBackgroundTaskWithExpirationHandler
As for your second question, you should set a breakpoint where code is supposed to run when you bring the app back to the foreground. No one can figure out why an app crashes if not given enough code or information.
The solution to the second part of the question was to raise the scope of the object containing the callback code. I raised it to the level of the containing ViewController. This seems to work. I still can't figure out how to raise the Notification (i.e. execute the call back) if the notification comes while the app is in the background/suspended. This prevented the object containing the callback from being cleaned up.

iOS bonjour programming - wait for a response

I am pretty new to bonjour/networking with ObjC (although well versed in other areas!) I am asking for a bit of advice - I have an iOS app that will run on multiple iPads, in a store. The apps occasionally have to share some data, and the internet isn't always available so a webservice is not an option, hence I decided on using bonjour.
I have setup the Bonjour/NSNetservices and everything is functioning correctly, the ipads basically form an 'ad-hoc network' and connect automatically at app launch, however I am looking for advice for the following situation:
The app normally shares data in the background, without any user intervention - however there is one function where when a button is pressed on one app, data should be returned from another app remotely. The UI then updates when the data has been received from the other device - however if the connection should be lost to the other device, the data will never reach the users device, and the data will not be displayed. I am wanting to implement some form of timout, but unsure how to do this - any suggestions would be much appreciated!
The process flow for this is something like this:
button press on 'dev 1' > 'dev 1' broadcasts 'dev 2 please send data message' > 'dev 2' responds with requested data [timeout required here] > UI is updated if data is received /[if timeout fires, error message is displayed]
So I really just need a timeout for the last section - and I really cannot think of a way to implement it.
I can post code for this if required.
Thanks!
This solution may work if you have the possibility to manually cancel the request.
I believe you can use a simple NSTimer to cancel the request after a wait. I'm sure it's not the best solution, but it will probably work.
Here's how I would do it.
Create your timer and an NSInteger (to store the timer value) in your class :
NSTimer *timer;
NSInteger timerValue;
Call this method with timer = [self startTimer]; when you fire your request :
- (NSTimer*)startTimer {
timerValue = 30;
return [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(timerTicked:) userInfo:nil repeats:YES];
}
Implement the timerTicked: method :
- (void)timerTicked:(NSTimer*)timer {
timerValue --;
if (timerValue <= 0) {
// Cancel the request
// Show an alert
}
}
You can cancel the timer with [timer invalidate]; but remember this will "destroy" your timer, so it won't fire events ever again.
As you've not indicated how you are currently requesting data from your other device, I have had to make some assumptions.
You can use NSURLRequest with a timeout using requestWithURL:cachePolicy:timeoutInterval:
See NSURLRequest documentation

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.

Resources