Setting Bundle not outputting correct value - objective-c

Here's the issue, I launch my app from Xcode and it gets up and running then I switch to settings.app and change a toggle from NO to YES and when I switch back to my app, the key outputs NO not YES.
i think i'm running up against the quote below, but not sure how to get around it, if the user launches the app, and goes to settings and changes the toggle, its now out of sync because Settings.app outputs on first launch to NO. Doesn't make sense that a user can't change the setting the first time they switch to settings.app
"For newly installed applications, default preference values from the application’s Settings bundle are not set until the Settings application runs. This means that if the user runs your application before running Settings, the default values specified in your Settings bundle are unavailable."
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:#"NO" forKey:#"hideActionBar"];
[defaults registerDefaults:appDefaults];
[defaults synchronize];
and then the code i use to check it
- (void)applicationDidBecomeActive:(UIApplication *)application {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL hidden = [defaults boolForKey:#"hideActionBar"];
NSLog(#"%d",hidden);
if (hidden) {
viewController.actionButton.enabled = NO;
} else {
viewController.actionButton.enabled = YES;
}
}
and my settings Root.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Hide Action Bar</string>
<key>Key</key>
<string>hideActionBar</string>
<key>DefaultValue</key>
<false/>
<key>TrueValue</key>
<true/>
<key>FalseValue</key>
<false/>
</dict>
</plist>

For BOOL variables you need not to set any value if want it to be NO. Because when you will try to access it first time it will return you the same. So remove registerDefaults code(all four lines) from your applicationDidFinishLaunchingWithOptions method.
Explanation:
When you change setting from NO to YES and launch your application, in applicationDidFinishLaunchingWithOptions this value is once changed to NO programmatically.
Note: Whenever you use settings bundle in your application, prior to registering a value(object value, because primitive types will return 0) check whether that value is nil(it means still unregistered), if the value for key is nil then only register with initial default values else you will end up in changing the values each time programmatically.
Thanks,

Related

iOS 8 requestWhenInUseAuthorization no Popup

I tried to make my AppProject iOS 8 ready. I had read a lot about
[_locationManager requestWhenInUseAuthorization];
and the entry in plist
NSLocationWhenInUseUsageDescription
So I changed all the necessary code lines.
It works fine, but now I have copied my project again from my iOS 7 base to include new features. But when I make the changes for the iOS8 Location Privacy the Popup doesn't appear anymore.
My code worked until I copied.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>tolle sache </string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>fapporite.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
and here is my call
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
_UserLocation = [[CLLocation alloc]init];
_locationManager = [[CLLocationManager alloc]init]; // initializing locationManager
_locationManager.delegate = self;
_locationManager.desiredAccuracy = kCLLocationAccuracyBest; // setting the accuracy
[_locationManager requestWhenInUseAuthorization]; // iOS 8 MUST
[_locationManager startUpdatingLocation]; //requesting location updates
NSLog(#"passed initwithcode");
}
return self;
}
How can I fix this?
From the documentation
NSLocationWhenInUseUsageDescription (String - iOS) describes the
reason why the app accesses the user’s location normally while running
in the foreground. Include this key when your app uses location
services to track the user’s current location directly. This key does
not support using location services to monitor regions or monitor the
user’s location using the significant location change service. The
system includes the value of this key in the alert panel displayed to
the user when requesting permission to use location services.
This key is required when you use the requestWhenInUseAuthorization
method of the CLLocationManager class to request authorization for
location services. If the key is not present when you call the
requestWhenInUseAuthorization method without including this key, the
system ignores your request.
This key is supported in iOS 8.0 and later. If your Info.plist file
includes both this key and the NSLocationUsageDescription key, the
system uses this key and ignores the NSLocationUsageDescription key.
Read about it here.
I find that the easiest way to add this key to your info.plist is to right click you info.plist and choose
Open As->Source Code
and then add the following in the end before </dict></plist>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
If you want you can add a text in between <string></string> that describes to the user why you want to use his/hers location. This text will show up under the default text in the alert.
Try writing a NSLocationWhenInUseUsageDescription in Info.plist
iOS 8.3, Xcode 6.3.1, ARC enabled
The question has been resolved, but I have (2) notes to add from my recent involvement with CLLocationManager.
1) You must have the following keys entered into your *.plist file:
Most commonly used keys have generic more descriptive names, such as "Privacy - Location Usage Description", which really is the "NSLocationUsageDescription" key.
To see the "raw" key names go to "*-Info.plist" and right click in the Navigator area where the keys are listed, see below:
And you get the following results:
The three keys that are related to this article are:
2) Make certain that you allocate and initialize your implementation of CLLocationManager before you try to request authorization or update location.
*.h file:
#interface SomeController : UIViewController <CLLocationManagerDelegate>
#property (strong, nonatomic) CLLocationManager *locationManager;
*.m file:
- (IBAction)locationButton:(UIButton *)sender
{
if (self.locationManager == nil)
{
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
}
else
{
nil;
}
if ([self.locationManager respondsToSelector:#selector(requestWhenInUseAuthorization)])
{
[self.locationManager requestWhenInUseAuthorization];
}
else
{
nil;
}
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[self.locationManager startUpdatingLocation];
}
Hope this saves someone time! Thanks.
Here's a little gotcha. Make sure you're adding the NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription keys to the main bundle plist and not one of your test target plists!
Had the same problem caused because I was instantiating CLLocationManager in a local var inside a method, solved it making the CLLocationManager a class property.
After a while I found the solution here, but I'm leaving it here since this is the first result in google, hope I save you some time:
requestWhenInUseAuthorization() not Work in iOS 8 With NSLocationWhenInUseUsageDescription Key in Info.plist
Pertaining to the Apple Watch:
You need to put the requestWhenInUseAuthorization key in the iPhone's Info.plist, not the WatchKit App's.
This has bitten me twice now.

Copy a file in Cocoa App with / without sandboxing enabled

First Issue: No Sandboxing
I am having an issue with some code to copy a file. With sandboxing turned off completely and this code.
- (IBAction)installWidget:(id)sender
{
// copy widgets to users library
NSError* error = nil;
NSString *testUrl = #"~/Library/Widgets/test.wdgt";
if ([[NSFileManager defaultManager]fileExistsAtPath:testUrl]) {
NSLog(#"yes");
}
NSString *testUrl2 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: #"test.wdgt"];
if ([[NSFileManager defaultManager]fileExistsAtPath:testUrl2]) {
NSLog(#"yes");
}
[[NSFileManager defaultManager] removeItemAtPath:testUrl error:nil];
[[NSFileManager defaultManager]copyItemAtPath:testUrl2 toPath:testUrl error:&error];
if (error != nil) {
NSLog(#"%#", [error localizedDescription]);
}
}
With sandboxing turned off I get the following error.
The file “test.wdgt” doesn’t exist. I tried zipping it thinking it was a flat file issue when changing the code from test.wdgt to test.zip I got the same error. The file is included in the resources bundle but its not letting me copy it.
I tried moving the app from outside of the build folder, same issue. I also tried cleaning the build folder same issue.
Second Issue: With Sandboxing
I get the following error with Sandboxing enabled. You don't have permission to access the directory Widgets.
Entitlement file looks like this
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.assets.movies.read-write</key>
<true/>
<key>com.apple.security.assets.music.read-write</key>
<true/>
<key>com.apple.security.assets.pictures.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.print</key>
<true/>
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
<string>/Users/jon/Library/Widgets/</string>
</array>
</dict>
</plist>
I am beating my head against the wall. I tried adding a simple image and then replacing the image name with the widget name and it copied fine as long as sandboxing was disabled. I wrote a widget thats works with my app and I want to distribute it by allowing the user to install the widget by clicking a button in the app or from a menu item. Any help is appreciated.
On the non-sandbox question, you need to expand the tilde(~) before using that in a path for file system operations. One way to do this is by calling -stringByExpandingTildeInPath on the string you have created, but the best practice for getting folders like this would be to use:
NSArray *paths = NSSearchPathForDirectoriesInDomains
( NSLibraryDirectory, NSUserDomainMask, YES);
This will return an array (should only be 1 element) containing the string to the path for the User's Library directory. Then you can add your specific path elements to that by using -stringByAppendingPathComponent:.
Thus, you'd get the full path by taking:
NSString *widgetsPath = [[paths objectAtIndex: 0] stringByAppendingPathComponent:
#"Widgets"];
As for doing in the sandbox, your current code will fail due to the wrong directory, which won't have access (since ~ is being interpreted as a path component and not to replace the User's home directory). However, you obviously can't use the absolute-path exception in shipping code by enumerating every user. Chances are you will need to find another approach to installing this if you are going to be sandboxed. You may be able to just open the widget file and thus get the OS to offer to copy it for you. Otherwise, you're going to have to ask the user's permission in some way, such as by popping up an open window and passing in the path to the user's Widget folder.

NSUserDefaults failing to persist across app shutdown/startup

EDIT: Code updated to reflect Paul.s's suggestions.
Have been scouring SO for the past week and still cannot get my app to persist the changes to settings from within the app across a shutdown/restart of the app in both Sim and Device.
Trying to use a NSMutableDictionary from a Singleton, but I don't think this is the problem.
a) Set initial default values for the app using registerDefaults in AppDelegate's applicationDidFinishLaunchingWithOptions:
AppManager *global = [AppManager sharedInstance];
// set up the defaults.
global.gWantFoo = YES; // This is a BOOL which relates to a UISwitch
// #define kWantFoo #"gWantFoo"
global.globalSettingsDict = [[NSMutableDictionary alloc]
initWithObjectsAndKeys:
[NSNumber numberWithBool:global.gWantFoo], kWantFoo, nil];
global.globalSettings = [NSUserDefaults standardUserDefaults];
[global.globalSettings registerDefaults:global.globalSettingsDict];
[global.globalSettings synchronize];
b) In the ConfigView where I am able to set the switch, I have the a selector execute the following on a switch change.
self.global.gWantFoo = NOT(self.global.gWantFoo); // #define NOT(a) !(a)
[self.global.globalSettings setBool:self.global.gWantFoo forKey:kWantFoo];
[self.global.globalSettings synchronize];
c) I have a synchronize in applicationDidEnterBackground and friends, and while the switch value does keep the change for the duration of the app instance's life, once I restart, gWantFoo is overwritten with the default of "YES".
Any advice is appreciated. I'm on the verge of defenestrating my ailing macbook pro with a hail of expletives following it closely behind. Ok, i'm calmer for this edit. I feel like gently shaking the macbook pro to demonstrate that i'm less than pleased with NSUserDefaults :-)
Cheers
sc.
So from start to finish this is what you need to do/what should be happening
In application:didFinishLaunchingWithOptions: one of the first things you do is register defaults like this:
NSDictionary *defaultsDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithBool:NO] , PSWantFoo, nil];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults registerDefaults:defaultsDefaults];
Note: PSWantFoo is defined as NSString * const PSWantFoo = #"PSWantFoo"; so i don't have string literals littered everywhere.
At this point if the app has never been run NSLog(#"%d", [defaults boolForKey:PSWantFoo]); it will print 0.
Now when I want to set the value I use something like:
[defaults setBool:YES forKey:PSWantFoo];
[defaults synchronize];
Now when I run NSLog(#"%d", [defaults boolForKey:PSWantFoo]); it will print 1.
As for persistance at this point as I have actually set a value a plist is created for me at
<path to app>/Library/Preferences/<bundle identifier>.plist
If you inspect this file you will see something like
<?xml version="1.0" encoding="UTF-8"?>
<DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PSWantFoo</key>
<true/>
</dict>
</plist>
Now from this point on this value will be read as opposed to the one registered in registerDefaults:
Update
Why have you hardcoded this in this order?
// set up the defaults.
global.gWantFoo = YES; // This is a BOOL which relates to a UISwitch
Why not do it the other way round?
Register defaults with default value YES;
NSDictionary *defaultsDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kWantFoo, nil];
[defaults registerDefaults:defaultsDefaults];
and then after that you get the setting
global.gWantFoo = [defaults boolForKey:kWantFoo];
This way if the app has not yet set the preference the answer will be YES otherwise it will be what the app has previously set it to.
You should set the flag #"gSettingsAreSet" after defaults values are saved
[[NSUserDefaults standardUserDefaults] setObject:#"YES" forKey:#"gSettingsAreSet"];
[[NSUserDefaults standardUserDefaults] synchronize];

Getting (default) value from settings bundle

I have an IOS5 project.
I have added a settings bundle to my project and added some settings to it.
I set properties of 'host_ip':
<dict>
<key>AutocapitalizationType</key>
<string>None</string>
<key>AutocorrectionType</key>
<string>No</string>
<key>DefaultValue</key>
<string>http://localhost</string>
<key>IsSecure</key>
<false/>
<key>Key</key>
<string>host_ip</string>
<key>KeyboardType</key>
<string>URL</string>
<key>Title</key>
<string>Host</string>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
</dict>
I try to read value like this:
NSUserDefaults *userDefaults =[NSUserDefaults standardUserDefaults];
NSString *host = [userDefaults stringForKey:#"host_ip"];
NSLog(#"%#",host);
It does not return the default value I set in host_ip, it returns nil.
How can I get my default value?
UPDATE
After a user edits a setting, I can retrieve it by the code above. My problem is getting the setting value, if it was not edited by the user yet. As I think in that case it should return the default I set in plist.
Did you register the defaults via NSUserDefaults?
See the registerDefaults: method on NSUserDefaults.

How do you create and access array elements in settings bundle

I want to save strings as array elements in a Settings.bundle Root.plist and be able to access those strings from the Setting bundle on subsequent launches of my app. I could not find a good example of this anywhere. In xcode, I have created the Root.plist and it looks something like:
Key
iPhone Setting Schema
Strings Filename
Preference Items
Item 0 (Title- My Title)
Type Title
Title My Title
Identifier My Identifier
Values
Item 0 my string 1
Item 1 my string 2
Item 3 my string 3
This produces xml as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Root</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSTitleValueSpecifier</string>
<key>Title</key>
<string>My Title</string>
<key>Key</key>
<string>My Identifier</string>
<key>Values</key>
<array>
<string>my string 1</string>
<string>my string 2</string>
<string>my string 3</string>
</array>
</dict>
I am using the following code to attempt an access of the Values from the Identifier key "My Identifier":
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *mystrings = [defaults arrayForKey:#"My Identifier"];
for (int i=0;i<3;i++) {
NSLog(#"%#", [mystrings objectAtIndex:i]);
}
The value of mystrings is 0 at runtime. Consequently, the Values of "my string 1", "my string 2", and "my string 3" are not getting printed by the NSLog.
Can someone help me with this. Also, what would be the correct way to update those values?
Part of your problem is mentioned under the heading "Specifying Default Values for Preferences":
default preference values from the application’s Settings bundle are not set until the Settings application runs. This means that if the user runs your application before running Settings, the default values specified in your Settings bundle are unavailable.
All the stuff in the Settings bundle doesn't get loaded for your app's defaults database until after the user opens the Settings Application. Does this seem stupid to you? Seems stupid to me. The result is that you need to register all those defaults initially yourself. I wrote this while I was trying to figure out what was going on; improvements can definitely be made, but it should get you started:
NSURL * settingsURL = [[NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:#"Settings" withExtension:#"bundle"]]
URLForResource:#"Root" withExtension:#"plist"];
NSDictionary * settingsDict = [NSDictionary dictionaryWithContentsOfURL:settingsURL];
NSArray * settingsArr = [settingsDict objectForKey:#"PreferenceSpecifiers"];
NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
for( NSDictionary * setting in settingsArr ){
NSString * key = [setting objectForKey:#"Key"];
if( !key ) continue; // Some objects don't have keys
if( ![defaults objectForKey:key] ){
[defaults setObject:[setting objectForKey:#"DefaultValue"]
forKey:key];
}
}
Another problem that you have is that you don't get an array for PSTitleValueSpecifier keys when you do [defaults objectForKey:#"myTitlePreferencesKey"]; you just get one string. A #"DefaultValue" key/value pair is also required to be part of that item's dictionary. Initially you will get that value, which must be a string. If you change the value, you'll get the string from the Titles array which has the same index as that object in the Values array. I'm pretty sure that if you have a Values array, you also have to have a Titles array.
For the second part of your question, if you want to change the values of any of the preferences, you simply do
[[NSUserDefaults standardUserDefaults] setObject:myValue
forKey:#"myKey"];