I have a code like this in my app:
NSString* version = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
In most cases it works, and returns the Bundle version, but sometimes (let's say in 2% of cases) it returns nil.
The code is called within the function [AppDelegate application:didFinishLaunchingWithOptions:], in Main thread, an app is in foreground.
I could imagine this being an Apple's bug with some file reading error, but the percentage is quite high as for a rare Apple bug.
Also I know a one could mess with versions/bundles/Info.plist - but the percentage is too small for such case.
So, the first question: what can be the reason of [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey] returning nil in this case?
The second question: do you know if these hypotheses make sense / are easy to check:
The user launches an app the first time after update, and [NSBundle mainBundle] becomes fully configured after application:didFinishLaunchingWithOptions ?
The app is in the process of an auto-update (from AppStore), the user opens it, and the system is currently writing a new data to Info.plist.
Some background thread in my app is also reading the [NSBundle mainBundle], the system uses some weird lock, so the read from a Main thread fails.
UPD: I've seen this question, but it's not related.
By default all files of the application, on a device that uses content protection, are encrypted. If you try to read them before they are decrypted(by the OS) you will get nil. The files are decrypted and they are available shortly after the user unlocks her phone, this might change and be more strict if you set a different value for the data protection entitlement. So the data might not be available when the application launches because they are not yet decrypted. This might be one reason that you get nil some times, the solution would be to wait to be notified in the app delegate that they data are ready before you read them.
The answers to all parts of the second questions are NO. There is no documented configuration that the app needs to do after an update that might affect the main Bundle. The user can not open an application if it is in the process of updating. The access to the main Bundle is thread-safe.
Related
I'm using NSUserDefaults in my iOS app to record some specific info about the user's receipt state.
I'd like to confirm:
If the user quits the app, will those defaults remain?
Are they global, for example I'm currently using the following line to either get or set them and across different methods. I just want to be certain the data within it persists - so if I set in method1 then later method2 I use the same line to get, it will have whatever I set in method1:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
If the user quits the app, will those defaults remain?
Yes, they are persistent.
Are they global?
Global in the sense of your whole app: Yes.
Global in the sense of across apps: No. They are in the app's sandbox.
From the NSUserDefaults documentation (see https://developer.apple.com/documentation/foundation/nsuserdefaults?language=objc)
With the exception of managed devices in educational institutions, a user’s defaults are stored locally on a single device, and persisted for backup and restore. To synchronize preferences and other data across a user’s connected devices, use NSUbiquitousKeyValueStore instead.
So for your first question the answer is yes.
Accordind to your second question, doc says:
At runtime, you use NSUserDefaults objects to read the defaults that your app uses from a user’s defaults database. NSUserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value. When you set a default value, it’s changed synchronously within your process, and asynchronously to persistent storage and other processes.
And even so, it declares that the class is thread safe, so you can be sure about persistent results (for your second answer).
Additionally with #Nikolai Ruhe answer.
if I set in method1 then later method2 I use the same line to get, it will have whatever I set in method1: NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
The UserDefaults class is thread-safe.
Two further points.
The uselessness of synchronize has grown up gradually through versions of iOS. It used to be essential; then advisable; then unnecessary. I can’t give you an exact timeline but a lot of it depends on how far back in iOS you want your app to work.
If you try to test some of these things in Xcode, beware. Up to and including iOS 11, synchronize does not write all the way to disk, but only puts data in a “lazy write” queue. Pressing the Stop button in Xcode (or pressing Run and allowing Xcode to stop the previous running app automatically) shuts everything down abruptly, more abruptly than anything a user can do, and the lazy writes are not written out, but lost. This is confusing while you are resting!! I filed a report on it and was told by Apple that (as Microfot would put it) “This behaviour is by design”.
But just to be clear: that second point does not present a problem for real apps in a real environment, only when you are trying to test “save and restart” scenarios. Waiting 5-10 seconds seems to be long enough for the flushed data to make it all the way to disk.
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.
I am calling a parent app on my iPhone from an Apple Watch app using openParentApplication and handleWatchKitExtensionRequest. In the main app, I use CoreData with the following options for addPersistentStoreWithType:
NSDictionary *options = #{
NSMigratePersistentStoresAutomaticallyOption : #YES, //
NSInferMappingModelAutomaticallyOption : #YES, //
NSSQLitePragmasOption : #{#"journal_mode" : #"DELETE"}, //
NSPersistentStoreFileProtectionKey : NSFileProtectionCompleteUnlessOpen
};
This caused an exception:
This NSPersistentStoreCoordinator has no persistent stores (device
locked). It cannot perform a save operation.
Does this mean that I can neither use NSFileProtectionCompleteUnlessOpen nor NSFileProtectionComplete?
Do I have to use NSFileProtectionNone or NSFileProtectionCompleteUntilFirstUserAuthentication?
I would like to know a way to protect my data by using NSFileProtectionCompleteUnlessOpen and still be able to access the data when my Watch app uses openParentApplication.
Possible ways to deal with the problem (but not a real solution)
Have two files (e.g., SQL data bases), where one is encrypted and the other one is not. The latter one would store only the data required by the Watch app.
NSFileProtectionCompleteUntilFirstUserAuthentication seems to be the recommended way for me. It makes sure the user has to unlock the device at least once since the last boot.
This problem was introduced with iOS 7 and background refresh. It's to prevent physical forensic analysis to read your unencrypted data.
Additionaly information from https://security.stackexchange.com/questions/57588/iphone-ios-7-encryption-at-lock-screen:
NSFileProtectionNone: file can be accessed any time, even if device is locked;
NSFileProtectionComplete: file can accessed only when device is unlocked (note there's ~10 seconds grace period after device is locked during which files are still accessible);
NSFileProtectionCompleteUnlessOpen: file can be created while device is locked, but once closed, can only be accessed when device is unlocked;
NSFileProtectionCompleteUntilFirstUserAuthentication: file can be accessed only if device has been unlocked at least once since boot.
The guys from Gilt also explained a lot about this behaviour here: http://tech.gilt.com/post/67708037571/sleuthing-and-solving-the-user-logout-bug-on-ios
Another idea which just came into my mind is to use an app group container. See the question here: WatchKit SDK not retrieving data from NSUserDefaults This way it should not only share NSUserDefaults but also the same keychain. This should work the same way to iOS Apps share the same keychain.
I use [NSUserDefaults standardDefaults] to store a boolean to see if it is the first time that application is being launched ... If so, the app should show a registration window.
It was working fine till last week, but now, sometimes when I switch to other apps and come back after a little while, I see that registration page loads while it shouldn't.
I used NSLog to see what is stored in [NSUserDefaults standardDefaults] and I see that the values I stored has been set to nil (null) while I haven't done that anywhere in my code.
Does anyone know why the values do reset ?
P.S: Actually the values are not permanently lost , because if I don't do anything in registration page and quit the app instead, It will launch normally the next time I enter the app !!!
A long time ago I encountered this issue, turns out a third party library that I was using uses the same key when storing values to NSUserDefaults. Try searching your project for this key, maybe something else is resetting it.
Here are the ways I know about to lose values in NSUserDefaults, in order of likelihood:
The key was never saved in the first place
Your app deletes it later on
The app is deleted and reinstalled
Another app overwrites or removes the key
The phone or simulator is reset
During the night, the phone is replaced by an identical-looking, different phone
It sounds like, from the discussion here, that you've ruled out 1,2,4, and probably 3 & 5. The only next debug step I can think of is to store the test phone in a locked drawer at all times.
But I'd leave my money on an intermittent problem causing #1. For that, we'd need posted code to investigate.
EDIT -
A high % of NSUserDefaults problems posted here are about storing BOOLs and other scalar types. It looks like the OP knows about wrapping in NSNumbers, but BOOLS in particular are fraught because it's easy to confuse false-y values like NO no and nil, and NSNull instance.
Let's throw that on the list for this question at #2.5. There again, would need code to confirm.
If this is happening while testing, it's normal. The fact that the program is even making this decision (should I show the registration page?) suggests that the app has been forcibly quit and is starting from scratch. When testing, this can result in clearing out the app sandbox as the app is reloaded from Xcode. In the real life of a real user, however, that won't happen (unless the user deletes the app from the device).
Make sure you are calling [[NSUserDefaults standardUserDefaults] synchronize] just after setting preferences and you are not overwriting you preferences.
I would like the determine if I have access to files that have been saved with the attribute NSFileProtectionKey = NSFileProtectionCompleteUntilFirstUserAuthentication.
I have tried [UIApplication sharedApplication].protectedDataAvailable, however from my tests it will return NO whenever the device is locked (if a pin code is set) even if the user has unlocked the device at least once since last starting the phone.
It says quite clearly in the docs: The value of this property is NO if data protection is enabled and the device is currently locked, and in this case files that were assigned the NSFileProtectionComplete or NSFileProtectionCompleteUnlessOpen protection key cannot be read or written by your app. i.e. this is not the correct property.
You need use one of the multi-tasking keys that causes the launch the app on boot - e.g. the voip key for UIBackgroundModes.
Mind you, you're pretty much testing the OS at that point - if you set the keys appropriately when creating the files it should work as advertised. If it doesn't then log a radar.