NSUserDefaults standardUserDefaults not working with extension - ios

I added App Groups to my app ID in the developer portal and am using that App ID in my provisioning profile. My Product Identifier in Xcode is set to that app ID.
In my app delegate I call this from didFinishLaunchingWithOptions
NSUserDefaults.standardUserDefaults().setObject("hey", forKey: "TEST")
NSUserDefaults.standardUserDefaults().synchronize()
In my keyboard app extension I call this:
if let test = NSUserDefaults.standardUserDefaults().objectForKey("TEST") as? String
{
println(test)
}
This never is true. If I remove the validation test and just print the result the custom keyboard crashes.
EDIT
Have also tried this with same crash result:
App Delegate
var userDefaults = NSUserDefaults(suiteName: "group.jackedgames.myappgroup")
userDefaults.setObject("TEST", forKey: "TEST")
userDefaults.synchronize()
Keyboard Extension
var userDefaults = NSUserDefaults(suiteName: "group.jackedgames.myappgroup")
var test = userDefaults.objectForKey("TEST") as String
NSLog(test)
In the "Capabilities" section of both targets I have the groups enabled with this group ID selected.
What am I missing here?

Set
RequestsOpenAccess = YES;
Settings:
NSUserDefaults * usrInfo = [[NSUserDefaults alloc] initWithSuiteName:#"myKeyboard"];
[usrInfo setObject:theme.icon forKey:#"themeName"]; // This is the new data;
[usrInfo synchronize];
keyboardChange:
NSUserDefaults * usrInfo = [[NSUserDefaults alloc] initWithSuiteName:#"myKeyboard"];
[usrInfo synchronize];
NSString * str = [usrInfo objectForKey:#"themeName"];
Then you can change the keyboard , for example ,change its background

Remove your keyboard from the iOS settings menu and add it again with "allow full access permission". It should work.
If it still doesn't work, check for both containing application and keyboard extension in target configuration in "Capabilities" section in app group that there are everything is fixed.

Just stepped on this. In keyboard extension you need "Full Access" to have access to write something to the app group files. So without "Full Access" you need to use NSUserDefaults.standardUserDefaults
The problem is that when you're changing NSUserDefaults from app group - it is working until you restart the app, because it stores values in memory too. Hard to debug.
I'm saving value to both: app group NSUserDefaults and NSUserDefaults.standardUserDefaults. And when reading - checking both too.

Related

iOS Firebase CrashlyticsKit not setting user ID

I am trying to set a user ID for Firebase Crashlytics reports.
Currently I send a user ID only if it's not sent or is changed (very rare event). And there is no user ID in crash reports.
My code:
+ (void)setCrashlyticsUserData:(User *)user
{
if (user == nil) { return; }
NSString *userIdKey = #"CRASHLYTICS_SENT_USER_ID";
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
NSInteger sentUserId = [userDefaults integerForKey:userIdKey];
if (sentUserId == user.userId) { return; }
[CrashlyticsKit setUserIdentifier:[NSString stringWithFormat:#"%i", user.userId]];
[userDefaults setInteger:user.userId forKey:userIdKey];
}
If this line is commented if (sentUserId == user.userId) { return; } I receive a user ID in crash reports.
Should I call [CrashlyticsKit setUserIdentifier:] every app launch? I can't find any information about it in the documentation.
Custom attributes of Crashlytics (like custom keys or user identifier) works in log-style in per-session basis.
So, you should call setUserIdentifier in each app session as early as possible.
See this link for code example:
https://fabric.io/kits/ios/crashlytics/features
You can use [Crashlytics setUserIdentifier:] to provide an id number, token, or hashed value that uniquely identifies the end-user of your application without disclosing or transmitting any of their personal information. You can also clear the value by setting it to a blank string. This value is displayed right in the Crashlytics dashboard.
[CrashlyticsKit setUserIdentifier:#"123456789"]; //User ID
Or another option is setthe custom key using [CrashlyticsKit setObjectValue:forKey:], See the following example.
[CrashlyticsKit setIntValue:3 forKey:#"userid"];
[CrashlyticsKit setObjectValue:#"logged_in" forKey:#"last_UI_action"];
See this document for more information.
https://docs.fabric.io/apple/crashlytics/enhanced-reports.html

Get "No keychain available" error when try to access keychain from app extension

I am making a IOS custom keyboard extension. By default the keyboard has limited functionality, but when the user purchases the full version from the main parent app then the keyboard unlocks the missing parts.
To check if the user has purchased the app I try to store a Bool in the IOS keychain using this library.
let IAPKeychain = Keychain(service: "com.roymunsonstudios.CopypastaKeyboard")
var purchase: Bool = true
let purchaseData = NSData(bytes: &purchase, length: sizeof(Bool))
try! IAPKeychain.set(purchaseData, key: identifier)
I then try to access the keychain from my keyboard extension in the viewDidLoad method like this:
let IAPFetch = Keychain(service: "com.roymunsonstudios.CopypastaKeyboard.Copypasta")
print("Just finished creating keychain instance")
do {
let purchaseStatusData = try IAPFetch.getData("com.roymunsonstudios.CopypastaKeyboard.FullVersionKeyboard")
}
catch is ErrorType {
print("Error fetching data")
}
However, whenever I try to do this I get the following error:
OSStatus error:[-25291] No keychain is available. You may need to restart your computer.
I am not sure what is wrong. I have turned on keychain sharing in the extension and the main app and I have the same keychain group name in each target (the keychain group name is the extension's bundle ID).
Why is this error occurring?

Why does iOS get a new identifierForVendor when app updates?

Every time my app is updated from the App Store some small number of the users get a new identifierForVendor for some reason. My users don't sign up or login. They are all anonymous so I need to separate them through their vendor IDs.
I've considered that there could've been insufficient space on some devices, resulting in the app being deleted and reinstalled, but that's not the case since in the last update a friend of mine had over 2GB of empty space.
I know that the identifierForVendor is changed for a user who deletes and reinstalls the app. But that's not the case here, as the app is just updated.
What could the problem be? The weird part is that this hasn't happened for development team devices yet. Or there are still users who haven't experienced this bug after countless app updates, OS updates, etc. This only happens to a small percentage of the users. All users are iOS7+ and it happens for different device models and iOS versions.
I use this code to get their ID:
static let DeviceId = UIDevice.currentDevice().identifierForVendor.UUIDString
Then I save them into NSUserDefaults:
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "User" + DeviceId)
NSUserDefaults.standardUserDefaults().synchronize()
Then I check if the user exists, at every new login:
static func doesUserExist() -> Bool {
var userDefaultValue: AnyObject? = NSUserDefaults.standardUserDefaults().valueForKey("User" + DeviceId)
if defaultValue == true {
println("Userdefaults already has this guy, moving on")
FirstTime = false
return true
} else {
println("First time in the app!")
FirstTime = true
return false
}
}
If the user does exist it starts the login process. If the user does not exist it shows them the signup process. I am using Parse.com as backend and the deviceID is used as a username. When that small amount of users experience this bug, I see a new username and a new account created.
There was a bug affecting the calculation of identifierForVendor when updating an app from app store between May and July. Apple has claimed they have already solved the issue and pushing another update should restore the original value before the critical date.
Reference: https://openradar.appspot.com/22677034
Take note that some users still have observed this issue even updating weeks after July. So it is still possible that bug is still there for some or it could resurface anytime in the future. So if you are using this data as part of your encryption, best is to save this to the keychain.
Save the vendorID in the KeyChain, that will persist after deletion or any update.
-(NSString *)getUniqueDeviceIdentifierAsString
{
NSString *appName=[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleNameKey];
NSString *strApplicationUUID = [SSKeychain passwordForService:appName account:#"incoding"];
if (strApplicationUUID == nil)
{
strApplicationUUID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
[SSKeychain setPassword:strApplicationUUID forService:appName account:#"incoding"];
}
return strApplicationUUID;
}
Disclaimer: I took the code from some other SO answer a while ago, so
I don't remember whom to praise, but in the end it's not me
#Jonny found the source
I'm confused why you're saving the IFV to NSUserDefaults first and then checking to see if that key exists. I think you should...
1) check NSUserDefaults first to see if the IFV key you created exists
2) if NSUserDefaults key does exist, great do what you need with it, this user's profile already exists
3) if key does not exist, get the IFV and save it to NSUserDefaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults valueForKey:#"IFV"]) {
//this user already exists, do what you need to do next with IFV
}
else{
//this is their first time using the app
NSString *ifvString = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
[defaults setValue:ifvString forKey:#"IFV"];
//do what you need to do next with IFV
}

iOS - WatchKit how to send message/data from iPhone app to WatchKit app?

I am creating a WatchKit app and was wondering how to send a message/data from the iPhone to the Watch?
I know how to do it the other way around (watch -> phone) using 'openParentApplication:reply:' and 'application:handleWatchKitExtensionRequest:reply:' but can't find any documentation on how to communicate from phone to watch.
Simple setup would be the iPhone app has a button that when pressed should update a label on the Watch app.
Can anyone point me in the right direction?
First, you have to enable app groups for your target:
Then you can start to write and read objects via NSUserDefaults:
// write
let sharedDefaults = NSUserDefaults(suiteName: appGroupName)
sharedDefaults?.setInteger(1, forKey: "myIntKey")
// read
let sharedDefaults = NSUserDefaults(suiteName: appGroupName)
let myIntValue = sharedDefaults?.integerForKey("myIntKey")
See the chapter Sharing Data with Your Containing iOS App in Apple Watch Programming Guide: Developing for Apple Watch
It's work for me. Try use in watch
- (void)registerToNotification
{
[ self unregisterToNotification ];
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), didReceivedDarwinNotification, CFSTR("NOTIFICATION_TO_WATCH"), NULL, CFNotificationSuspensionBehaviorDrop);
}
- (void)unregisterToNotification
{
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)( self ), CFSTR( "NOTIFICATION_TO_WATCH" ), NULL );
}
void didReceivedDarwinNotification()
{
// your code
}
in main app
- (void)sendNotificationToWatch:(NSDictionary*)info
{
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFSTR("NOTIFICATION_TO_WATCH"), (__bridge const void *)(self), nil, TRUE);
}
You should try App Groups which are what you use to share data between iOS apps and App Extensions.
In your Apple Watch app interface controller class:
let sharedDefaults = NSUserDefaults(suiteName: "group.com.<domain>.<appname>.AppShare")
sharedDefaults?.setObject("Came from Apple Watch App", forKey: "AppleWatchData")
sharedDefaults?.synchronize()
In your parent app:
let sharedDefaults = NSUserDefaults(suiteName: "group.com.<domain>.<appname>.AppShare")
if let appWatchData = sharedDefaults?.objectForKey("AppleWatchData") as? NSString {
println(appWatchData)
}
"AppShare" is the name you assign when you create an App Group in Capabilities for your parent app target.
watchOS 2.0 has a new framework which is called Watch Connectivity Framework that let you send messages between the two devices.
That framework provides a bidirectional communications channel for sending files and data dictionaries between the two processes
Please see the example here including the example of sending the actual dictionary using debug mode.
A WiKi example is also available.
Good luck.
Alternatively,
You can use this solution to share files even between 2 different apps and of course between watch app (Extension) and parent iOS app.
First step is described by #zisoft, enable app group.
Then get the URL of group container at runtime,
- (NSString *)containerPath
{
return [[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"YOUR_APP_GROUP"] relativePath];
}
Now you can write any file/folder at the given path, Below is my example snippet,
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.array];
NSString *path = [[[self containerPath] stringByAppendingPathComponent:#"Library"] stringByAppendingPathComponent:#"history"];
if ([data writeToFile:path atomically:YES])
{
NSLog(#"Success");
}
else
{
NSLog(#"Failed");
}

How to detect whether custom keyboard is activated from the keyboard's container app?

I was wondering if there is a method that would allow me to detect from the keyboard container app whether the associated keyboard has been activated in the the device's Settings app.
For example, I am interested in adding a simple "steps" feature inside the container app where step 1 would be "activate the keyboard", and step 2 would be contingent on step 1's completion. As such, I am interested in figuring out whether there is a way to detect whether the keyboard extension is activated?
Thanks!
Here is a method I have used in one of my projects. I think it is what you asked for, hope it helps you.
- (BOOL)isCustomKeyboardEnabled {
NSString *bundleID = #"com.company.app.customkeyboard"; // Replace this string with your custom keyboard's bundle ID
NSArray *keyboards = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] objectForKey:#"AppleKeyboards"]; // Array of all active keyboards
for (NSString *keyboard in keyboards) {
if ([keyboard isEqualToString:bundleID])
return YES;
}
return NO;
}
Just in case here is Swift version of Kurt's brilliant and awesome answer:
func isKeyboardExtensionEnabled() -> Bool {
guard let appBundleIdentifier = Bundle.main.bundleIdentifier else {
fatalError("isKeyboardExtensionEnabled(): Cannot retrieve bundle identifier.")
}
guard let keyboards = UserDefaults.standard.dictionaryRepresentation()["AppleKeyboards"] as? [String] else {
// There is no key `AppleKeyboards` in NSUserDefaults. That happens sometimes.
return false
}
let keyboardExtensionBundleIdentifierPrefix = appBundleIdentifier + "."
for keyboard in keyboards {
if keyboard.hasPrefix(keyboardExtensionBundleIdentifierPrefix) {
return true
}
}
return false
}
The current documentation states By default, your extension and its containing app have no direct access to each other’s containers.
It is also stating that the container app can share data with the keyboard in the following fashion:
// Create and share access to an NSUserDefaults object.
NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc]
initWithSuiteName:#"com.example.domain.MyShareExtension"];
// Use the shared user defaults object to update the user's account.
[mySharedDefaults setObject:theAccountName forKey:#"lastAccountName"];
Read more on this: Communicating and persisting data between apps with App Groups
Obstacle no 1: According to the documentation, for this to work, the RequestsOpenAccess in the plist needs to be set to YES as it would gain the following capability:
Option to use a shared container with the keyboard’s containing app,
which enables features such as providing a custom lexicon management
UI in the containing app
Requesting full access for a simple case like this is definitely not preferred on my side.
Obstacle no 2: Using this knowledge of setting a NSUserDefault, leaves me to think of a method where this can be set in place. But there's no public method indicating an extension is installed. So this is a dead end for now.
--
[Update 1]
Not super relevant but still worth stating: the shouldAllowExtensionPointIdentifier app delegate method in combination with the constant UIApplicationKeyboardExtensionPointIdentifier can deal with disallowing custom keyboards. The extension point identifiers are not unique identifiers of the extension but of their type.
Read more on this: Can I disable custom keyboards (iOS8) for my app?
--
[Update 2]
Another question with same issue, but w/o solution: How to detect an app extension is enabled in containing app on iOS 8?
--
This is a work-in-progress answer stating my findings so far which I hope to be updating coming days should I find a solution.
You can use this function (Swift 3 and 4) to check your custom keyboard extension have open access or not:
func isOpenAccessGranted() -> Bool{
if #available(iOS 10.0, *) {
let originalString = UIPasteboard.general.string
UIPasteboard.general.string = "Sour LeangChhean"
if UIPasteboard.general.hasStrings {
UIPasteboard.general.string = originalString ?? ""
return true
}else{
UIPasteboard.general.string = ""
return false
}
} else {
// Fallback on earlier versions
if UIPasteboard.general.isKind(of: UIPasteboard.self) {
return true
}else{
return false
}
}
}

Resources