iOS - KeyValueStore not working across devices - ios

I have an app that will implement key value store in order to save some variables and sync across devices.
I have a view controller with the following:
var keyStore: NSUbiquitousKeyValueStore?
On viewDidLoad I do
keyStore = NSUbiquitousKeyValueStore()
Then when a user finishes watching a video I run
keyStore?.set(num, forKey: "watched") // where num is a number
keyStore?.synchronize()
In order to check the value of the "watched" variable, I'm running this on didFinishLaunchingWithOptions:
keyStore = NSUbiquitousKeyValueStore()
let val = keyStore?.string(forKey: "watched")
print(val)
If I test the app several times, the value of val changes accordingly. However, when I test on a different device, val returns nul. If I use the application in this new device, the value for "watched" changes but values never sync between the two devices.
I read that there is a Notification Method to check if the stored value changed in real time, but in this case I'm reading the stored value during app launch. Wouldn't that make it unneeded?

My bad. Make sure you are logged into Cloud in your simulator.

Related

Intermittent data loss with background fetch - could NSKeyedUnarchiver return nil from the documents directory?

I have a simple app that stores an array of my custom type (instances of a class called Drug) using NSCoding in the app’s documents folder.
The loading and saving code is an extension to my main view controller, which always exists once it is loaded.
Initialisation of array:
var drugs = [Drug]()
This array is then appended with the result of the loadDrugs() method below.
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
// Save to shared container (for iMessage, Spotlight, widget)
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
}
Here is the code for loading data.
func loadDrugs() -> [Drug]? {
var appContainerDrugs = NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
return appContainerDrugs
}
Data is also stored in iCloud using CloudKit and the app can respond to CK notifications to fetch changes from another device. Background fetch also triggers this same method.
// App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// Code to get reference to my main view controller
// This will have called loadDrugs() to populate local array drugs of type [Drug]
mainVC.getZoneChanges()
}
Finally, there is the getZoneChanges() method, which uses a stored CKServerChangeToken to get the changes from the private user database with CKFetchRecordZoneChangesOperation. The completion block calls saveDrugs().
The problem
All of this seems to work fine. However, sometimes all local data disappears between uses of the app, especially if it has not been used for some time. Deleting and reinstalling the app does pull the backed-up data from iCloud thankfully.
It seems to happen if the app has not been used for a while (presumably terminated by the system). Something has to have changed, so I presume it is the calling of a background fetch when the app is terminated that may be the problem. Everything works fine while debugging and when the app has been in foreground recently.
Possible causes
I’m guessing the problem is that I depend on background fetch (or receiving a CK notification) loading my main view controller in the background and then loading saved local data.
I have heard that UserDefaults does not work correctly in the background and there can be file security protections against accessing the documents directory in this context. If this is the case here, I could be loading an empty array (or rather initialising the array and not appending the data to it) and then saving it, overwriting existing data, all without the user knowing.
How can I circumvent this problem? Is there a way to check if the data is being loaded correctly? I tried making a conditional load with a fatal error if there is a problem, but this causes problems on the first run of the app as there is no data anyway!
Edit
The archive URLs are obtained dynamically as shown below. I just use a static method in my main data model class (Drug) to access them:
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("drugs")
The most common cause of this kind of issue is being awakened in the background when the device is locked and protected data are encrypted.
As a starting point, you can check UIApplication.isProtectedDataAvailable to verify that protected data is available. You can also lower the protection levels of data you require to .completeUntilFirstUserAuthentication (the specifics of how to do that depends on the how you create your files).
As a rule, you should minimize reducing data protection levels on sensitive information, so it's often best to write to some other location while the device is locked, and then merge that once the device is unlocked.
The problem is that you are storing the full path of the file and not just the fileName (or relative path). The document directory URL can change, and then if you stored that URL persistently you will not be pointing to the correct location of the file. Instead just store the filename and use NSFileManager URLsForDirectory to get the documents directly every time you need it.

UIPasteboard not giving value

An object that helps a user share data from one place to another within your app, and from your app to other apps.
This is the statement written at the very beginning of UIPasteboard docs. But when I try to use it in two different apps accessing data set by other app I am getting nil everytime
DispatchQueue.global(qos: .background).async {
var i = 1
while(i > 0) {
let v = UIPasteboard.general.string
sleep(1)
print("Task : \(i)")
print("Value: \(v)")
i = i + 1
}
}
I am fetching data in above code and setting data as in below code.
UIPasteboard.general.string = "Hello"
NB: I have tested locally in this app it is setting data
Are you running iOS >=10? There was a privacy change regarding passing value between apps. Try reading the UIPasteBoard api doc : (https://developer.apple.com/documentation/uikit/uipasteboard).
Tl:dr You need to have both apps to be in the same app group (Communicating and persisting data between apps with App Groups)
To note: iOS apps are sandboxed. So the change in iOS 10 just enforces that feature.
EDITED: Since you can't use App Groups (different developer and/or products), you have to send data via a different channel. Try searching urlSchemes or store/fetch through a common server(tedious tho)

iOS 11.4 not asking Privacy Usage ( Privacy - Motion Usage Description has been set )

I'm stumped, iOS 11.4 ( 15F79 ), iPhone 6. Cannot get the App to Ask for Motion Data. info.plist has been set via the editor and double checked via the info.plist open in textWrangler, Also deleted key and saved via textWrangler.
<key>NSMotionUsageDescription</key>
<string>This app needs your Phones motion manager to update when the phone is tilted. Please allow this App to use your phones tilt devices</string>
I have deleted then reinstalled the app about 10 times. I have restared the phone 5 times. I have checked through settings and my app does NOT show up in Privacy-Motion and Fitness or anywhere else in settings. I am using a free developer account, maybe that has something to do with it?
I created a new Xcode game template and changed nothing apart from importing CoreMotion and this code
**** Edited, sorry I forgot to say I had started the instance, just forgot to put it here, just in case someone thinks that's the problem ************
let motionManager = CMMotionManager()
override func didMove(to view: SKView) {
motionManager.startDeviceMotionUpdates()
if motionManager.isDeviceMotionActive == true {
motionManager.accelerometerUpdateInterval = 0.2
motionManager.startAccelerometerUpdates(to: OperationQueue.current!, withHandler: {
(accelerometerData: CMAccelerometerData!, error: NSError!) in
let acceleration = accelerometerData.acceleration
print(accelerometerData)
} as! CMAccelerometerHandler)
}else{
print(CMMotionActivityManager.authorizationStatus().rawValue)
}
which prints a 0 ( an Enum - case not determined ) to the console.
In my actual app it was a 3 ( same Enum - case Denied ).
As I've said, I have uninstalled, reinstalled, edited plist via Xcode and text wrangler ( a code editor ) , tried different versions of the code above, tried the code in different places ( in did move to view, in class )tried code off apple docs. etc.... I haven't been asked the NSUsage question and the App keeps crashing.
I have looked for ways to get the Alert fired up, As in CLLocationManager.requestWhenInUseAuthorization() but I cannot find a comparable CMMotion version ( I don't think there is one. ) I have created a new swift file , imported Foundation and CMMotion and just put that code there, But still no Alert asking for Motion Data.
I tried a single view app template instead of a game template thinking that might be the issue, Nope.
What do I do?
Any help Appreciated. Thanks
You are confusing two related but different classes.
CMMotionManager gives access to accelerometer, magnetometer and gyroscope data. It does not require any user permission as this information is not considered privacy related.
In your else clause you are checking the authorisation status of CMMotionActivityManager. This object reports the device motion type (walking, running, driving). This information is considered privacy related and when you create an instance of this class and request data from it, the permissions alert is displayed.
The reason your else is being triggered is because you are checking isDeviceMotionActive; this will be false until you call startDeviceMotionUpdates, which you never do. Even if you used isAccelerometerActive you would have a problem because you call startAccelerometerUpdates in the if clause which will never be reached.
You probably meant to check isAccelerometerAvailable. If this returns false then there isn't much you can do; the device doesn't have an accelerometer.
Update
It doesn't make sense to check isDeviceMotionActive immediately after calling startDeviceMotion:
You know it's active; you just started it
I imagine the start up takes some time, so you could expect to get false if you check immediately.
Apple recommends that you do not have more than one observer in place for each motion device type, so the purpose of check the is...Active to ensure you don't call start... again if you have already done so.
If you only want gyroscope data then you don't need to call startDeviceMotionUpdates at all.

iOS Notification Content Extension - How to pass data to app?

I wrote a custom Notification Content Extension for my Push Notifications like this:
The thing is, whenever the user is on a certain item in the carousel, I want the GO TO APP button to send a String to the app when it's opening, and from there, handle that string to move the user to the correct ViewController.
I already have the handling part inside the app, I just need to know how to pass that String from the Notification Content Extension to the container app.
Thanks! :)
Enable app groups in capabilities and use suite userDefaults to write the key and read it in the app
NSUserDefaults*defaults= [[NSUserDefaults alloc] initWithSuiteName:#"group.com.company.appName"];
// Write in extension
[defaults setObject:#"anyThing" forKey:#"sharedContent"];
// Read in app
[defaults objectForKey:#"sharedContent"];
If your app is configured for Universal Links or you have defined a Custom URL Scheme for your app, you can also open your app's URL (e.g. with data in query parameters) by calling
extensionContext?.open(url)
in your NotificationViewController.
iOS 13, Swift 5.
Based on the answer by Sh_Khan, here is some Swift Syntax. Obviously I have added App Group as a capability to the target of the main app + the target of the extension, naming the group as "group.ch.Blah" for this example.
Setting your app group, saving a string in our case, needed to set the type as Any cause strings not a type that is available in groups.
let localK = getPrivateKey64() as Any
let defaults = UserDefaults.init(suiteName: "group.ch.Blah")
defaults?.set(localK, forKey: "privateK")
Setting your app group, and reading the string back, needed to recast it back to string.
let defaults = UserDefaults.init(suiteName: "group.ch.Blah")
let localK = defaults?.object(forKey: "privateK") as? String
Worked perfectly with a notification service extension.

iOS 7+: Unique identifier per device<->Store-ID (user) combination

I've spent two days no reading and testing as there is a lot of info about this topic.
Unfortunately I've found no solution yet. I can't implement my own authentication as this doesn't help with the issue I want to solve (see Backgrounding at the end of the question).
Here is my current best approach:
I'm generating a UUID thanks to https://stackoverflow.com/a/8677177/1443733 and storing it in the KeyChain as suggested with SwiftKeychainWrapper (https://github.com/jrendel/SwiftKeychainWrapper)
The short nice and sweet code for that is:
let stored = KeychainWrapper.stringForKey("UUID")
if stored != nil {
Helper.log(TAG, msg: "retrieved from keychain: \(stored!)")
} else {
let theUUID = CFUUIDCreate(nil)
let str = CFUUIDCreateString(nil, theUUID)
Helper.log(TAG, msg: "generated UUID: \(str)")
let ret = KeychainWrapper.setString(str, forKey: "UUID")
Helper.log(TAG, msg: "setkeychain: \(ret)")
}
But the UUID stored in the keychain seems to be per device and not per store ID as well.
When I store the UUID like above and login with a different Store ID on the device KeychainWrapper.stringForKey("UUID")still returns the value of the other user.
Isn't their a way to store a value in a store-id keychain?
It seems that I'm so close so I hope someone could point me in the right direction.
If that approach (with a keychain) can't succeed please let me know as well.
I reckon you can ask a different question as well: Is there some cryptic data I can read/generate or a store which changes with the Store Id currently used on a device?
Ohh... and Swift examples preffered ;)
Backgroundinfo:
I use IAPs in my app and want to save the Store-Id of the user once a refresh of the receipt is valid.
On each start of the app I check if the current Store-Id is the same as the saved one. If not I trigger immediately a refresh of the receipt. If it fails I fall back to the free version of the app.
iOS devices do not support multiple users.
If you want to differentiate between users you will have to do that in your app, perhaps with a user login. Then save a UUID per userID in the Keychain.
As NSUserdefaults temporarily stores the UUID.so,after unistalling the app and again installing,the UID changes. So, UUID must be stored in keychain

Resources