Unable to read values from Settings.bundle

TigreFurry picture TigreFurry · Jul 21, 2011 · Viewed 7.7k times · Source

I've been passively using this site for some time and have read the FAQ so I hope that I will be able to keep these at a minimum.

I've a question about Settings.bundle files in Objective-C. I've been using the following guides to implement one of those:

  • How to create an iPhone preferences file
  • iOS Application Programming Guide - Implementing Application Preferences
  • I'm using both Xcode 4.1 and the latest Xcode 3. I used version 3 to create the Settings.bundle and its plist as I found Xcode 4's plist editor to be somewhat buggy.

    These are the contents of my 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>StringsTable</key>
    <string>Root</string>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
            <key>Title</key>
            <string>Authentication</string>
        </dict>
        <dict>
            <key>Type</key>
            <string>PSTextFieldSpecifier</string>
            <key>Title</key>
            <string>User ID</string>
            <key>Key</key>
            <string>user_id_key</string>
            <key>DefaultValue</key>
            <string>1234</string>
            <key>IsSecure</key>
            <false/>
            <key>KeyboardType</key>
            <string>Alphabet</string>
            <key>AutocapitalizationType</key>
            <string>None</string>
            <key>AutocorrectionType</key>
            <string>No</string>
        </dict>
    </array>
    </dict>
    </plist>
    

    I can access the settings in the iOS settings menu without a problem and also change the value of the single entry that I have so far. The value gets stored between sessions on the iOS simulator as well as on my physical iOS device (an iPhone 4).

    I do however seem to be unable to access this value from my code. Let me show you how I tried to implement it (in the ViewController.m of my app):

    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        myDefaults = [NSUserDefaults standardUserDefaults];
    
        // DEBUG
        userID = [myDefaults stringForKey:@"user_id_key"];
        // userID = [NSString stringWithString:@"1234"];
    
        // DEBUG
        jsonBackendURL = @"http://example.com/jsonBackend.php";
        passphrase     = @"secretPassphrase";
    
        theURLConnectionController = [[URLConnectionController alloc] initWithCallerObject:self];
        [theURLConnectionController getConnectionAndUpdate:NO];
    }
    

    Strangely enough when I try to run the app in Xcode 3 I neither get any error messages (like a SIGABRT or such) nor any debugger output. The screen of the iPhone Simulator goes black for a moment as it is trying the start the app and then I'm being taken back to the home screen.

    Xcode 4 shows some rather odd behavior. It manages to show the UI of my application for a second or two and then takes me back to the home screen of the iPhone Simulator. It also indicates a SIGABRT some way farther down in my code where I try to get a value from a JSON response:

    NSString *arrivalToday = [NSString stringWithString:[jsonResponse objectForKey:@"from"]];
    

    As the JSON string is generated by a PHP script using GET parameters passed in by my app - one of these the user ID from the Settings.bundle - it is of little wonder that this shouldn't work. It shouldn't crash however as I catch this error earlier in my code but that's likely not connected to my settings problem. A real pain in the neck though is the fact that Xcode (kind of) crashes along with my app in the iPhone Simulator. First, Xcode keeps working normally except the fact that I can't abort the program which still appears to run. I can click the stop button in Xcode but to no effect. When I try to exit Xcode it asks me if it should terminate the running program and when I click yes it crashes for real and I have to force quit it.

    Fortunately though, Xcode 4 gives me a call stack of my app:

    2011-07-21 11:41:04.539 OneClickCheckIn[2098:b303] http://example.com/json.php?user=(null)&pass=secretPassphrase&getStartToday
    2011-07-21 11:41:04.888 OneClickCheckIn[2098:b303] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSPlaceholderString initWithString:]: nil argument'
    *** Call stack at first throw:
    (
    0   CoreFoundation                      0x00de25a9 __exceptionPreprocess + 185
    1   libobjc.A.dylib                     0x00f36313 objc_exception_throw + 44
    2   CoreFoundation                      0x00d9aef8 +[NSException raise:format:arguments:] + 136
    3   CoreFoundation                      0x00d9ae6a +[NSException raise:format:] + 58
    4   Foundation                          0x007ca14c -[NSPlaceholderString initWithString:] + 105
    5   Foundation                          0x007d3266 +[NSString stringWithString:] + 72
    6   OneClickCheckIn                     0x00002b5c -[OneClickCheckInViewController updateInterfaceWithJsonResponse:] + 172
    7   OneClickCheckIn                     0x0000e82c -[URLConnectionControllerDelegate connectionDidFinishLoading:] + 364
    8   Foundation                          0x007de112 -[NSURLConnection(NSURLConnectionReallyInternal) sendDidFinishLoading] + 108
    9   Foundation                          0x007de06b _NSURLConnectionDidFinishLoading + 133
    10  CFNetwork                           0x030fa48e _ZN19URLConnectionClient23_clientDidFinishLoadingEPNS_26ClientConnectionEventQueueE + 220
    11  CFNetwork                           0x031c56e1 _ZN19URLConnectionClient26ClientConnectionEventQueue33processAllEventsAndConsumePayloadEP20XConnectionEventInfoI12XClientEvent18XClientEventParamsEl + 293
    12  CFNetwork                           0x031c59cf _ZN19URLConnectionClient26ClientConnectionEventQueue33processAllEventsAndConsumePayloadEP20XConnectionEventInfoI12XClientEvent18XClientEventParamsEl + 1043
    13  CFNetwork                           0x030f0c80 _ZN19URLConnectionClient13processEventsEv + 100
    14  CFNetwork                           0x030f0acf _ZN17MultiplexerSource7performEv + 251
    15  CoreFoundation                      0x00dc38ff __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 15
    16  CoreFoundation                      0x00d2188b __CFRunLoopDoSources0 + 571
    17  CoreFoundation                      0x00d20d86 __CFRunLoopRun + 470
    18  CoreFoundation                      0x00d20840 CFRunLoopRunSpecific + 208
    19  CoreFoundation                      0x00d20761 CFRunLoopRunInMode + 97
    20  GraphicsServices                    0x0101a1c4 GSEventRunModal + 217
    21  GraphicsServices                    0x0101a289 GSEventRun + 115
    22  UIKit                               0x00042c93 UIApplicationMain + 1160
    23  OneClickCheckIn                     0x00002609 main + 121
    24  OneClickCheckIn                     0x00002585 start + 53
    25  ???                                 0x00000001 0x0 + 1
    )
    terminate called throwing an exception(lldb) 
    

    That's all of the relevant information coming to my mind right now. I've done extensive research on Google and have also read some threads on this site but I couldn't find a solution.

    Answer

    TigreFurry picture TigreFurry · Jul 22, 2011

    I've finally been able to solve it thanks largely to the above mentioned guide: Adding a settings bundle to an iPhone App.

    There it says:

    [...] it is important to understand that until the user actually changes the value of the setting nothing is actually set. If you check for the setting in your application it will actually return nil unless you set a default value. To do that add the following to the applicationDidFinishLaunching (or didFinishLaunchingWithOptions) method:

    // Set the application defaults
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:@"YES" forKey:@"enableRotation"];
    [defaults registerDefaults:appDefaults];
    [defaults synchronize];
    

    I wasn't aware of that. Moreover it seems to me that the values in the NSDictionary have to initialized in some form or later changes by the user are not going to take effect either (I tried time and again to change the values in the iPhone Simulator while I'd been looking for a solution and it didn't work). This might only affect the iPhone Simulator though, I haven't had the chance to test it on my physical device yet. My initialization code now looks like this (in the AppDelegate):

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        // Override point for customization after application launch.
        NSUserDefaults *defaults  = [NSUserDefaults standardUserDefaults];
        NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
                                 @"1234", @"user_id_key",
                                 @"http://example.com/json.php", @"json_url_key",
                                 @"secretPassphrase", @"passphrase_key",
                                 nil];
    
        [defaults registerDefaults:appDefaults];
        [defaults synchronize];
    
        self.window.rootViewController = self.viewController;
        [self.window makeKeyAndVisible];
        return YES;
    }
    

    With that code I can easily access the NSUserDefaults values later in my code:

    - (void) reloadPreferences {
        myDefaults = [NSUserDefaults standardUserDefaults];
    
        userID         = [myDefaults objectForKey:@"user_id_key"];
        jsonBackendURL = [myDefaults objectForKey:@"json_url_key"];
        passphrase     = [myDefaults objectForKey:@"passphrase_key"];
    }
    

    I also spent some time hunting down a bug concerning the order of the key-value pairs of the NSDictionary until it dawned on me that the value comes first followed by the key in the definition. Coming from a Perl background this is somewhat alien to me so that might be something to watch out for if you're getting nil values from your NSUserDefaults object.

    This cost me about three days to fix so I hope that the solution might come in handy for some. :-)