I'm integrating the Dropbox Sync SDK into my iOS app. I want to set it up so that when the user is in a UIDocument, and then the app becomes inactive (home button, lock, etc.), and then the file is changed by someone else in Dropbox, and then the user returns to the app, they will be notified that changes were made elsewhere. Here's what I have now:
In my viewDidLoad I have:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(checkForNewerStatus) name:UIApplicationDidBecomeActiveNotification object:nil];
Then I have this method:
- (void)checkForNewerStatus
{
if (self.dropboxFile.newerStatus)
{
//alert user of changes
}
}
This works semi-desirably. When the app first comes back, self.dropboxFile.newerStatus returns NO. If I leave the app and come back a second time, then it returns YES. But I need it to return YES the first time the app comes back. This isn't time related - I can wait several minutes before coming back, and it still fails the first time and succeeds the second. Any ideas?
Many thanks!
Note: this problem only occurs if the app becomes inactive and then the file is changed. If the file is changed while the app is still active, and then the app leaves and comes back, it alerts as expected.
#rmaddy's comment above is correct. Just moving it into an answer.
You need to add an observer on your DBFile to get notified when it changes. What's happening is that your app is being restarted and your immediately checking the status, but since the app just started, it hasn't had time yet to talk to the server and learn about the newer file content. An observer on the DBFile will fire as soon as the app is able to communicate with the server and find out about the change.
Related
Currently, when I change the camera permissions for my app in Settings, then navigate back to my app, the app will force a refresh and I will lose my place in the app. I follow these steps exactly:
Open an app that uses the camera permission.
Navigate to some screen within the app (so you can visibly see the refresh later)
Go to the Settings app, navigate to the app's settings, and toggle
the camera permission
Double click home and go back to the app.
After a few seconds, it will refresh, bringing you back to the
first screen
Note: I'm using an iPhone 6 running iOS 8.4
I've noticed this behavior on all apps that have the camera permission. My question is:
Is there some way to prevent the app from refreshing/restarting (on resume) after changing the camera permission? It doesn't seem to happen when you toggle location services, for example, and from a usability perspective this is horrible.
User scenario: If a user navigates deep into your app, then needs to change the camera permission (because say they accidentally clicked no last time), they will be forced to navigate back to that screen when they return. This is especially harmful for an app trying to sell you something, or sign you up for a new account. They might try to introduce a new feature where you can use the camera to take a profile picture or scan your credit card. Because the user didn't know about this feature, they might have previously denied camera access, but now want to enable it. After trying to reenable, they come back to your app to find they have to spend 5+ minutes signing up / making a purchase, again! After that, even I would probably give up.
I'm sure that there is no other ways to prevent restarting app. Actually you will get a SIGKILL message but no Crash log when toggling settings. See below links-
https://devforums.apple.com/message/715855
https://devforums.apple.com/message/714178
The only way to prevent this scenario is to save the previous state of you application while terminating.
Store app your current data into a json/plist/NSUserDefaults/archive user model at applicationWillTerminate: method and
restore saved data at applicationWillEnterForeground:
For example- #SignUpViewController register for UIApplicationWillTerminateNotification which will fire when the app is about to terminate. Store user information there.
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationWillTerminate:)
name: UIApplicationWillTerminateNotification object:nil];
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
// store your data here
}
Hope this will help you:)
The accepted answer is correct, however the workaround does not appear to work in the current version of iOS (9.2) - the application seems to terminate before UIApplicationWillTerminateNotification is fired. However by listening to UIApplicationDidEnterBackgroundNotification, the same can be achieved. e.g in Swift, put this in viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "enteringBackground:", name: UIApplicationDidEnterBackgroundNotification, object: nil)
and have a function like this:
func enteringBackground(sender:AnyObject){
// Save application state here
}
I'm trying to reach the file which I was downloading earlier by NSURLSession. It seems I can't read the location of the file, even though I'm doing it before delegate method ends (as the file is temporary).
Still, I'm getting nil when trying to access the data under location returned from NSURLSession delegate and error 257.
The code goes as following:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSError *movingError = nil;
NSData *fileData = [NSData dataWithContentsOfFile:location.path options:0 error:&movingError]; // is nil
NSLog(#"%#", movingError); // is error 257
}
What's wrong with this code..? I saw similar questions NSURLSessionDownloadTask - downloads but ends with error and iPhone - copying a file from resources to Documents gives an error but these completely doesn't apply to my case.
-- edit --
I've created a new project and pasted the very same code. It works... So:
1) In my project I'm receiving error 257, probably some configuration of the project is invalid or the fact I'm using backgroundTasks somewhere else in the app messes things up
2) Same as in 1 happens if I put the source files of this download to the external framework linked in by Carthage
3) On demo project I created (copy-pasted files used in 1 & 2) everything works corretly.
If someone has an idea what can cause the fact it isn't working - would be awesome.
Late to the discussion, but I've seen the same/similar issue and investigated.
With my investigation, what you are seeing is expected (should I say “it's observed”?).
But since I'm not 100% sure how you are using URL session, I put down below what I'm trying to do, followed by my observations (important ones only).
Also, I created a sample project and put it here as downLoader.zip. You can play and check how URL session background download works. But It's better to read thru below before trying to play, though this is fairly a long note.
A. what I'm trying to do
I'm developing kind a map app, where I need to download 1000+ of small size (0.5-150kB, mostly ~20kB) PNG files at a time. Total download size of them is ~50MB, and it takes a couple of minutes to download all of them. I think that keeping users to my app just waiting for download is bad design, so I made the app to use URL Session’s background download.
I have to admit, however, that Apple’s doc says that “Background sessions are optimized for transferring a small number of large resources that can be resumed as necessary.” So, the way I’m using background session is totally opposite. But, anyway...
B. what I've observed
Below, I’ve listed my observations and my guess of why. I should say guess since they are not documented.
(1) observation: sometimes, files that should come with didFinishDownlaodingTo don’t exist.
Temporary files are put at:
/var/mobile/Containers/Data/Application/randomHexString/Library/Caches/com.apple.nsurlsessiond/Downloads/yourName.yourApp. The randomHexString changes from app’s run to run. When the files don’t exist, the randomHexString that came with didFinishDownloadingTo is the one from the “previous” session. Now, “previous” here means the session at app’s previous run !, which clearly doesn't exist at the current session at current run.
There's another scenario for non-existent file, which is that the randomHexString is ok, but the files don't exist. This seems to happen after session cancel is requested (invalidateAndCancel) and before session becomes invalid (didBecomeInvalidWithError).
Guess: This happens especially on the development phase since we terminate the app while downloading, manually or by debugger or just by bug. It seems once the download requests are handed and accepted to the OS, the OS handles our requests even after the app quits. Note we can’t know if the OS has definitely accepted the requests or not. Even after the return from URLSessionDownloadTask:resume(), sometimes files don’t exit at the next launch and sometimes they do.
Workaround: If files don’t exist, just ignore them. In a while, “this time’s” files should come.
(2) observation: sometimes, the duplicated (same) files come with didFinishDownloadingTo.
My app converts downloaded PNG files to other format. W/in didFinishDownlaodingTo, I move temp files (== OS designated) to another my app’s directory, then spawn thread (GCD) to convert the format and delete downloaded temp files. So, the other thread (didFinishDownlaodingTo) to overwrite is an issue.
Workaround: I make list of URLSessionTask:taskIdentifier and w/in didFinishDownlaodingTo, check the duplication by retrieving the taskID list to ignore duplicated files.
(3) observation: even after the user terminated the app, OS relaunches the app again.
After the user terminated the app from task switcher, quite often, the OS relaunches the app w/ application:handleEventsForBackgroundURLSession:completionHandler.
Note the sequence of relaunch is that didFinishLaunchingWithOptions comes first as regular launch, then handleEventsForBackgroundURLSession comes next.
From user’s POV, when he/she terminated the app, that’s it, done! It looks strange even after they terminated the app, it relaunches in itself and notifies something to them. It's like a zombie.
Guess: The Apple’s document says “If the user terminates your app, the system cancels any pending tasks”. The definition of “pending tasks” is not clearly stated but I understand this is from iOS POV and not user’s or program developer’s. As guessed in (1), iOS seemed to have accepted the download requests, and they are not “pending tasks” anymore.
Workaround: I create a “flagFile” after all of download requests are handed to iOS w/ URLSessionDownloadTask:resume(), of which file means that “download is ongoing”. When users terminate the app, at UIApplicationDelegate:applicationWillTerminate, I delete the flag file. Or, if all of download requests not handed to iOS, there's no flag file. Then at app’s relaunch at UIApplicationDelegate:handleEventsForBackgroundURLSession, I check if we have the flag file. If missing, then I can assume that users terminated. Two choices here. Choice-1: I will not recreate URL session. What happens next is that iOS will terminate my app in about 20 seconds. I have no idea if this (== not creating the URL session) is a legal operation but it works. Users can launch w/in this 20 sec, so I put some more code to handle that scenario. Choice-2: I create URL session. What happens next is that iOS calls delegate methods didFinishDownlaodingTo/didCompleteWithError, followed by urlSessionDidFinishEvents. If I don't do anything here, the process (app) keeps alive indefinitely w/o any notification to users: nothing in task switcher. This is nothing more than waste of memory. The option here is to fire local notification and let users know of my app, so that they can go back to my app and can terminate (Again!), though my app clearly appears as a zombie. One issue for both choices: applicationWillTerminate may not be called in certain situation (though I've yet confirmed). At this case, flag file is left as regular ops and show zombie to users. So, the flag file method is just mitigation to the issue, but I think it works most of the time for my app.
Note the app is relaunched sometimes when it's killed by xcode debugger or killed by OS w/ bug (SEGFALUT).
(4) observation: after the app is terminated (by user, etc.) then relaunched by OS, the app is occasionally in active state (UIApplication.shared.applicationState is .active).
I want to notify the user on the download completion by local notification, but since it's active, local notification doesn't fire. So, I need to use UIAlertController instead. Therefore, I can't provide consistent user experience, and should look strange for users: most of the time local notification and very occasionally UIAlert. Note when app started in active state, it appears in task switcher.
Guess: totally no idea how this can happen. One good(?) thing is this happens only occasionally.
Workaround: seems none.
(5) observation: handleEventsForBackgroundURLSession/urlSessionDidFinishEvents is called just once.
I start the download after started background task (application.beginBackgroundTask). Then in expiration handler of beginBackgroundTask, I call endBackgroundTask. I don't know exactly why, but after endBackgroundTask, my process is still given lots of process time, so I can keep requesting download. This might be because download files keep coming w/ didFinishDownlaodingTo. To be a good citizen, I suspend to request further download, and fire local notification to user to put the app to foreground. Now, once I suspend the request, in 4-5 seconds, OS determines the URL session is over and handleEventsForBackgroundURLSession then urlSessionDidFinishEvents are called. This is one-off event. When the user put the app in foreground to resume download, then put it again in background, no handleEventsForBackgroundURLSession/urlSessionDidFinishEvents will come anymore. What's unclear to me is the definition of session's start and end. It seems the session starts at first URLSessionTask.resume(), then ends w/ timeout, which seems to be determined by URLSessionConfiguration.timeoutIntervalForRequest. However, setting large number here (1000 sec) doesn't affect anything, it's always 4-5 seconds.
Guess: no idea
Workaround: don't relay on urlSessionDidFinishEvents while the app is alive. Relay only when app relaunched by OS and at initial handleEventsForBackgroundURLSession/urlSessionDidFinishEvents.
===============
Below, I listed about the sample project (downLoader.zip). You can verify all of above with the sample.
The app has a list of download files. The number of files is 1921, and 56MB in total. They are 256x256 PNG map tile files that are located in a server managed by Geo Spatial Information Authority of Japan (GSI). After downloaded, they are moved to Library/Cache/download. If your device is jail-broken, you can view them w/ Filza.
crash itself to test relaunch
emulate background task expiration
logging to file since debugger doesn't work after relaunch by OS. The file is in Documents, can be moved to PC.
Do play with the real device. Simulator doesn't relaunch the app.
Project built w/ Xcode 8.3.3 and tested w/ iphone6+/9.3.3 and iphone7+/10.3.1
To see if app relaunched w/ xcode, go to Debug/Attach to Process menu and see if downLoader is listed.
===============
I think URL session background download behaves trickily, especially at relaunch. We need to consider at least the observations I listed above, or the app users will get confused.
I found same error code 257 from online users feedback.In the error scene, the location always refers to a strange bundle identifier:com.sdyd.SDMobileMixSmart. And I guess this's an apple system bug.
Currently, when I change the camera permissions for my app in Settings, then navigate back to my app, the app will force a refresh and I will lose my place in the app. I follow these steps exactly:
Open an app that uses the camera permission.
Navigate to some screen within the app (so you can visibly see the refresh later)
Go to the Settings app, navigate to the app's settings, and toggle
the camera permission
Double click home and go back to the app.
After a few seconds, it will refresh, bringing you back to the
first screen
Note: I'm using an iPhone 6 running iOS 8.4
I've noticed this behavior on all apps that have the camera permission. My question is:
Is there some way to prevent the app from refreshing/restarting (on resume) after changing the camera permission? It doesn't seem to happen when you toggle location services, for example, and from a usability perspective this is horrible.
User scenario: If a user navigates deep into your app, then needs to change the camera permission (because say they accidentally clicked no last time), they will be forced to navigate back to that screen when they return. This is especially harmful for an app trying to sell you something, or sign you up for a new account. They might try to introduce a new feature where you can use the camera to take a profile picture or scan your credit card. Because the user didn't know about this feature, they might have previously denied camera access, but now want to enable it. After trying to reenable, they come back to your app to find they have to spend 5+ minutes signing up / making a purchase, again! After that, even I would probably give up.
I'm sure that there is no other ways to prevent restarting app. Actually you will get a SIGKILL message but no Crash log when toggling settings. See below links-
https://devforums.apple.com/message/715855
https://devforums.apple.com/message/714178
The only way to prevent this scenario is to save the previous state of you application while terminating.
Store app your current data into a json/plist/NSUserDefaults/archive user model at applicationWillTerminate: method and
restore saved data at applicationWillEnterForeground:
For example- #SignUpViewController register for UIApplicationWillTerminateNotification which will fire when the app is about to terminate. Store user information there.
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationWillTerminate:)
name: UIApplicationWillTerminateNotification object:nil];
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
// store your data here
}
Hope this will help you:)
The accepted answer is correct, however the workaround does not appear to work in the current version of iOS (9.2) - the application seems to terminate before UIApplicationWillTerminateNotification is fired. However by listening to UIApplicationDidEnterBackgroundNotification, the same can be achieved. e.g in Swift, put this in viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "enteringBackground:", name: UIApplicationDidEnterBackgroundNotification, object: nil)
and have a function like this:
func enteringBackground(sender:AnyObject){
// Save application state here
}
I have an iOS application where I need to persist a set of data after the application HARD CLOSES (when the user double clicks the Home button and slides the application upwards). Then, when the application comes back to the foreground, I need to fetch that data and do something with it. I'm just not sure where I need to put that logic for Application Hard Close and Resuming the Application.
In your AppDelegate
When your app is going to be closed, but still in Multitasking menu the following method is getting called
-(void)applicationWillResignActive:(UIApplication*)application
If after 3 minutes user doesn re-open your app this method is going to be called
-(void)applicationDidEnterBackground:(UIApplication*)application
If user re-opens your app from multitasking menu the following method is getting called
-(void)applicationWillEnterForeground:(UIApplication*)application
If user is going to close your app from multitasking menu this method is getting called(you will have limited time to perform some logic here)
-(void)applicationWillTerminate:(UIApplication*)application
When user presses home twice applicationWillResignActive and applicationDidEnterBackground is called. You can save data here.
When user opens app, applicationWillEnterForeground is called, you get data which you save and process.
When a user hard-closes the application, the UIViewController's Delegate's method called applicationWillTerminate. Here I can catch and save the model data before it's all destroyed.
Then when a user launches the application again, there are many choices like didFinishLaunchingWithOptions where I can grab the data stored to disk.
Your app no longer gets an applicationWillTerminate call ever. You are simply silently killed while in the background. You have to write your app to save state in the applicationDidEnterBackground handler, as nmh describes. That's your only option.
I'm doing some prefetching of data to reduce waiting time experience. In viewDidLoad of UIViewController A, I initiate an asynchronous function from a downloader class to call to the server to grab some small pieces of data. The downloader class stores this information in some Core Data tables. Then, when UIViewController B comes along, it uses that data to populate a table.
When I was running this on a simulator, it worked without exception. But as soon as I put it on a real phone and untethered the phone from the computer, the app crashed. When I got to VC B, I click on a button to open up that table. That button click would just hang, and the app would never recover. I checked the crash log and got the 0x8badf00d error. I'm doing my testing on an old iPhone 4, which could be part of my problem, but the app needs to run on all iPhone versions, not just newer ones.
OK, so I suspected why this was happening - the data wasn't back yet. As a quick and dirty way to test this, I put in a boolean user default that I set to NO from VC A. Then, when the downloader class got the data, it set that same variable to YES. In VC B in viewDidLoad, I put in a busy loop to make it wait until the value had been set to YES. When I ran the app, it instantly cleared the hanging problem, and the wait time in VC B was instant. The user would never know that this was present.
Despite this momentary success, I think that this approach of mine is terrible! Is there a better way than using user defaults? I don't know how to employ a delegate pattern here, and notifications might not work either. I realize that I am blocking the UI thread, but this is intentional. I can't let the user open that table before there is data available. Is there a more elegant approach that is more consistent with Objective C patterns?
I can post code if it will help. Thank you!
I would probably go with notifications for something like this and do your downloading in the background (I couldn't tell if you were doing it in the background or not).
To run the download code in the background do something like
[[DownloadManager instance] performSelectorInBackground:#selector(doDownload) withObject:nil];
In the doDownload class post a notification when it is done
//Downloading code
...
[[NSNotificationCenter defaultCenter] postNotificationName:#"DownloadsDidFinish" object:self];
In your VC B listen for the notification.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(downloadDidFinish:) name:#"DownloadsDidFinish" object:nil];
In downloadDidFinish: you may want to do a performSelectorOnMainThread depending on if you care if you are on a background thread or not at this. You can also post the notification on the main thread by doing the performSelectorOnMainThread at the time of posting the notification.
You can have ViewController B as a delegate with downloadedStarted and downloadedStopped methods. And you can start and stop spinning wheel animation in these methods:
[busyIndicator startAnimating];