Swift Problems - NSUserDefault and Settings Bundle not responding - ios

So I had set up a settings bundle to just do one thing. Allow the Users to choose between the The TouchUI and GestureUI in my app and for some odd reason, I am unable to get the Settings Bundle to control it. It stays with one and doesn't switch even when I have the If else Statement. originally I had var touchCheck = userDefaults.boolForKey("myGestureEnabledDisabled") but it didn't change boolean at all when i keep closing app(Multitask > SwipeUp app) and re running the app via springboard. The Settings App could have the bundle at NO but log says gestures are on. After watching a Tutorial, i changed boolForKey to valueForKey which causes the build to fail and there is no error in the code the way i have it below.
override func viewDidLoad() {
var userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.synchronize()
var touchCheck = Bool(userDefaults.valueForKey("myGestureEnabledDisabled"))
if touchCheck {
whenGuestureIsEnabled()
} else {
whenGestureIsDisabled()
self.navBar.hidden = false
}
}
func whenGuestureIsEnabled() {
NSLog("Gesture is suppose to be on")
}
func whenGestureIsDisabled() {
NSLog("Gesture is OFF")
}
Maybe from what I was thinking, I shouldn't do this in UIViewController but I had seen this in action in a youtube tutorial and it was in OBJ-C.

You should cast to bool, not to create a new one:
//notice (Bool) cast in the beginning
var touchCheck = (Bool)userDefaults.valueForKey("myGestureEnabledDisabled")

Related

UserDefaults.standard.string() returns nil

I "inherited" an iOS Xcode Project in Swift. I've never programmed in Swift or used Xcode before. I copied the project from one user account to another and checked it into a git repo.
Now I'm araid this (the copying, permissions ?) might be the cause for the App not being able to read its settings properly. Because UserDefaults.standard.string(forKey: "pref_Foo") returns nil although in the Settings.Bundle's Root.plist there's clearly a pref_Foo identifier.
The App has worked before, so I don't see where this suddenly comes from.
Since I'm not too familiar with XCode all I did up til now was debug into the Application to see that UserDefaults.standard.string(forKey: "pref_Foo") is nil.
How could I approach this problem?
Thank you!
EDIT: This is part of my code in ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
loadSettings()
...
}
func loadSettings()
{
if(UserDefaults.standard.object(forKey: "pref_Foo") == nil)
{
printf("Error")
}
}
You should use below code with validation, first delete app and install again. then use, you will get success.
var UserStatus:String {
get{
return UserDefaults.standard.object(forKey: "pref_Foo") as? String ?? "0"
}
set(status){
UserDefaults.standard.set(status, forKey: "pref_Foo")
UserDefaults.standard.synchronize()
}
}
Use of code is as below.
if (UserStatus == "pref_Foo"){
self.logoutSuccess()
} else {
self.loginSuccess()
}
Update or set value is so simple in one line code as given below.
UserStatus = "pref_Foo"

Build Failed: Value of optional type '[SFUserAccount]?' not unwrapped

A newbie to native swift development!
Opened the below issue in https://github.com/forcedotcom/SalesforceMobileSDK-iOS/issues/2072
Version of Mobile SDK Used: 5.1.0
Issue found in Native App or Hybrid App: Native App
OS Version: 10.12.5
Device: iPhone 6
Steps to reproduce:
forceios create
Provided application type as native_swift and add other requested details
Open the *.xcworkspace file in Xcode
Build the project
Error: Value id optional type '[SFUserAccount]?' not unwrapped;
func handleSdkManagerLogout()
{
self.log(.debug, msg: "SFAuthenticationManager logged out. Resetting app.")
self.resetViewState { () -> () in
self.initializeAppViewState()
// Multi-user pattern:
// - If there are two or more existing accounts after logout, let the user choose the account
// to switch to.
// - If there is one existing account, automatically switch to that account.
// - If there are no further authenticated accounts, present the login screen.
//
// Alternatively, you could just go straight to re-initializing your app state, if you know
// your app does not support multiple accounts. The logic below will work either way.
var numberOfAccounts : Int;
let allAccounts = SFUserAccountManager.sharedInstance().allUserAccounts()
numberOfAccounts = (allAccounts!.count);
if numberOfAccounts > 1 {
let userSwitchVc = SFDefaultUserManagementViewController(completionBlock: {
action in
self.window!.rootViewController!.dismiss(animated:true, completion: nil)
})
if let actualRootViewController = self.window!.rootViewController {
actualRootViewController.present(userSwitchVc!, animated: true, completion: nil)
}
} else {
if (numberOfAccounts == 1) {
SFUserAccountManager.sharedInstance().currentUser = allAccounts[0]
// ERROR: Value id optional type '[SFUserAccount]?' not unwrapped;
}
SalesforceSDKManager.shared().launch()
}
}
}
The allUserAccounts property of SFUserAccountManager is nullable.
- (nullable NSArray <SFUserAccount *> *) allUserAccounts;
https://github.com/forcedotcom/SalesforceMobileSDK-iOS/blob/master/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Security/SFUserAccountManager.h#L188
If you know for a fact that it will exist at the time you are trying to use it, you can perform a force unwrap by typing allAccounts![0]. If you need to handle the case where it may be nil, you need to check for that by doing something like:
if let accounts = allAccounts
{
currentUser = accounts[0]
}
else
{
// does not exist
}
What I can't tell you is whether it being nil is actually a valid case that you need to handle, as I am not familiar with the library. You'll need to do the research or ask them yourself.
You need to unwrap allAccounts because it's an optional array. And since force unwrapping has been used above to get numberOfAccounts, it's probably safe to use it.
Try this:
SFUserAccountManager.sharedInstance().currentuser = allAccounts![0]

Difficulty reading UserDefaults with Fastlane Snapshot

I'm using Fastlane's snapshot to create screenshots for an app I'm about to submit to the App Store.
It works "as advertised" for the most part, but it doesn't seem to like the way I access the UserDefaults within my app. On one test, it generates an Exit status: 65 error.
UI Testing Failure - com.me.MyApp crashed in (extension in MyApp):__ObjC.NSObject.defaultTime () -> Swift.Float
I find UserDefaults.standard.value(forKey: "defaultTime") to an invitation for a syntax error, so I created an extension to access UserDefaults. Here's what the extension looks like:
class CustomExtensions: NSObject {
/*
This is blank. Nothing else in here. No really...nothing else
*/
}
extension NSObject {
// User Defaults
func defaultTime() -> Float {
return UserDefaults.standard.value(forKey: "defaultTime") as! Float
}
// a bunch of other UserDefaults
}
Wihin the app, whenever I need defaultTime, I just type defaultTime(). Using this method to access UserDefaults values works fine in the Simulator and on the devices I've tested. I only encounter a problem with snapshot.
I've tried adding in sleep(1) within the test, but that doesn't seem to do anything. I welcome suggestions re: alternative means of accessing UserDefaults that enable me to access them easily throughout MyApp.
What's probably happening is that, in your simulator and on device, you're writing a value to user defaults for the key defaultTime before it is ever read. value(forKey: returns an optional, and if you force-unwrap it (or force down-cast as your are doing here), you will crash if the value is nil. Try either returning an optional:
func defaultTime() -> Float? {
return UserDefaults.standard.value(forKey: "defaultTime") as? Float
}
or using a default value:
func defaultTime() -> Float {
return UserDefaults.standard.value(forKey: "defaultTime") as? Float ?? 0.0
}

Today extension: syncing data with container app

Context
I've been playing around with Today Extensions using this example project.
The app is quite simple:
In the containing app, you have a list of todo items, which you can mark completed
In the Today widget, you see the same list, but you can switch between completed, and incomplete items using a segmented control.
My goal is the following: whenever there is a data change, either in the container app, or the widget, I want both to reflect the changes:
If I mark an item as completed in the container app, then pull down the Notification Center, the widget should be updated
When I do the same in the widget, then return to the app, the app's state should be updated
The implementation
I understand, that the container app, and the extension run in their separate processes, which means two constraints:
NSUserDefaultsDidChangeNotification is useless.
Managing the model instances in memory is useless.
I also know, that in order to access a shared container, both targets must opt-in to the App Groups entitlements under the same group Id.
The data access is managed by an embedded framework, TodoKit. Instead of keeping properties in memory, it goes straight to NSUserDefaults for the appropriate values:
public struct ShoppingItemStore: ShoppingStoreType {
private let defaultItems = [
ShoppingItem(name: "Coffee"),
ShoppingItem(name: "Banana"),
]
private let defaults = NSUserDefaults(suiteName: appGroupId)
public init() {}
public func items() -> [ShoppingItem] {
if let loaded = loadItems() {
return loaded
} else {
return defaultItems
}
}
public func toggleItem(item: ShoppingItem) {
let initial = items()
let updated = initial.map { original -> ShoppingItem in
return original == item ?
ShoppingItem(name: original.name, status: !original.status) : original
}
saveItems(updated)
}
private func saveItems(items: [ShoppingItem]) {
let boxedItems = items.map { item -> [String : Bool] in
return [item.name : item.status]
}
defaults?.setValue(boxedItems, forKey: savedDataKey)
defaults?.synchronize()
}
private func loadItems() -> [ShoppingItem]? {
if let loaded = defaults?.valueForKey(savedDataKey) as? [[String : Bool]] {
let unboxed = loaded.map { dict -> ShoppingItem in
return ShoppingItem(name: dict.keys.first!, status: dict.values.first!)
}
return unboxed
}
return nil
}
}
The problem
Here's what works:
When I modify the list in my main app, then stop the simulator, and then launch the Today target from Xcode, it reflects the correct state. This is true vice-versa.
This verifies, that my app group is set up correctly.
However, when I change something in the main app, then pull down the Notification Center, it is completely out of sync. And this is the part, which I don't understand.
My views get their data straight from the shared container. Whenever a change happens, I immediately update the data in the shared container.
What am I missing? How can I sync up these two properly? My data access class is not managint any state, yet I don't understand why it doesn't behave correctly.
Additional info
I know about MMWormhole. Unfortunately this is not an option for me, since I need to reach proper functionality without including any third party solutions.
This terrific article, covers the topic, and it might be possible, that I need to employ NSFilePresenter, although it seems cumbersome, and I don't completely understand the mechanism yet. I really hope, there is an easier solution, than this one.
Well, I have learned two things here:
First of all, Always double check your entitlements, mine somehow got messed up, and that's why the shared container behaved so awkwardly.
Second:
Although viewWillAppear(_:) is not called, when you dismiss the notification center, it's still possible to trigger an update from your app delegate:
func applicationDidBecomeActive(application: UIApplication) {
NSNotificationCenter.defaultCenter().postNotificationName(updateDataNotification, object: nil)
}
Then in your view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserverForName(updateDataNotification, object: nil, queue: NSOperationQueue.mainQueue()) { (_) -> Void in
self.tableView.reloadData()
}
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Updating your Today widget is simple: each time the notification center is pulled down, viewWillAppear(:_) is called, so you can query for new data there.
I'll update the example project on GitHub shortly.

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