NSUserDefaults proper usage and Detecting Change - iphone

I'm a bit of a beginner, and building an iOS Universal app which makes use of NSUserDefaults at various stages of the application lifecycle - including an in-App page to set these variables. I find that my UserDefaults are often out-of-sync with the UI, and it gets tedious to write code to check for these preferences.
As you can see below, I'm checking to see if the "Use Camera" preference is set an aweful lot; there must be an easier way...?
Get's worse when we load the viewController that allows in-app editing of user preferences:
Is there some way to eliminate some of the amount of code (and therefore, minimize debugging/effort for memory optimization), instead of having to check in to what the value of the preferences are a lot of the time? Should I be using the NSUserDefaultsDidChangeNotification to detected a change, and set AppDelegate variables once inside this notification method?
Example (omissions of the obvious assumes those methods are there... just not related to my preferences:
In my AppDelegate:
#progma mark Application Lifecycle
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
// check values; if not present, set some defaults.
if (![prefs stringForKey:#"pfUseCamera"]) {
[prefs setValue:#"NO" forKey:#"pfUseCamera"];
}
if (![prefs stringForKey:#"pfWorkInBackground"]) {
[prefs setValue:#"NO" forKey:#"pfWorkInBackground"];
}
[prefs synchronize];
self.backgroundTasksTimer = [NSString stringWithString:[prefs stringForKey:#"pfWorkInBackground"]];
self.shouldUseCamera = [NSString stringWithString:[prefs stringForKey:#"pfUseCamera"]];
// start our timer
// chose to run this timer ALL-the-time, dispite the user-preference; instead, see below that the timer method returns based on the user preference.
[NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:#selector(backgroundTasks:) userInfo:nil repeats:YES];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// pretty much the same as above
// except NSTimer, which is controlled elsewhere
}
#progma mark AppDelegate-controlled Methods
- (void)backgroundTasks:(NSTimer *)timer {
if (![self.backgroundTasksTimer boolValue]) {
return;
} else {
// do stuff in a timer begun in AppDelegate only if this preference is NOT NO
}
}
My primary ViewController:
#progma mark ViewController Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
MyAppDelegate *app = [MyAppDelegate sharedAppDelegate]; // I have this set up in the AppDelegate to allow this
if ([app.shouldUseCamera boolValue]) {
// init the camera and have it ready for use
}
}
- (void)viewDidAppear:(BOOL)animated {
MyAppDelegate *app = [MyAppDelegate sharedAppDelegate]; // I have this set up in the AppDelegate to allow this
if ([app.shouldUseCamera boolValue]) {
// resume the camera capture
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
MyAppDelegate *app = [MyAppDelegate sharedAppDelegate];
if ([app.shouldUseCamera boolValue]) {
// suspend the camera capture
}
}
- (void)viewDidUnload {
MyAppDelegate *app = [MyAppDelegate sharedAppDelegate];
if ([app.shouldUseCamera boolValue]) {
// unload the camera
}
}
#progma mark User/IBAction Methods
-(IBAction)doStuff:(id)sender {
MyAppDelegate *app = [MyAppDelegate *app = [MyAppDelegate sharedAppDelegate]; sharedAppDelegate];
// take a picture
if ([app.shouldUseCamera boolValue]) {
[NSThread detachNewThreadSelector:#selector(takePicture:) toTarget:self withObject:fileName]; // take a photo and do stuff to it in a separate thread
}
}
SettingsViewController:
- (void)viewDidLoad {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
MyAppDelegate *app = [MyAppDelegate sharedAppDelegate];
// set UITextField to value of String Preference
someUITextField.text = [prefs stringForKey:#"pfSomeOtherValue"];
// set UISwitch to value of PSToggleSwitchSpecifier
if ([app.backgroundTasksTimer boolValue]) {
[shouldExecuteBackgroundTasks setOn:YES];
} else {
[shouldExecuteBackgroundTasks setOn:NO];
}
// set UISwitch to value of PSToggleSwitchSpecifier
if ([app.shouldUseCamera boolValue]) {
[shouldUseCamera setOn:YES];
} else {
[shouldUseCamera setOn:NO];
}
- (IBAction)done:(id)sender {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
MyAppDelegate *app = [MyAppDelegate sharedAppDelegate];
[prefs setObject:companyID.text forKey:#"pfCompanyID"];
if ([shouldUseCamera isOn]) {
[prefs setObject:#"YES" forKey:#"pfUseCamera"];
app.backgroundTasksTimer = #"YES";
} else {
[prefs setObject:#"NO" forKey:#"pfUseCamera"];
app.backgroundTasksTimer = #"NO";
}
if ([shouldExecuteBackgroundTasks isOn]) {
[prefs setObject:#"YES" forKey:#"pfWorkInBackground"];
app.shouldUseCamera = #"YES";
} else {
[prefs setObject:#"NO" forKey:#"pfWorkInBackground"];
app.shouldUseCamera = #"NO";
}
[prefs synchronize];
[self.delegate settingsControllerDidFinish:self];
}

one approach is to get rid of the fields in your app delegate and just use NSUserDefaults. I'm not sure of the overhead with this but have a "Config" class with static accessors. Also, you might consider using the bool (and other) accessors like:
#implementation Config
+(BOOL)shouldUseCamera {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
return [defaults boolForKey:#"shouldUseCamera"];
}
+(void)setShouldUseCamera:(BOOL)should {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:should forKey:#"shouldUseCamera"];
}
Then it's cake to access these from anywhere. I'll use NSBundle and NSProcessInfo in the same Config class and then my app has a single place for all config.
Also, from what I've read, you don't normally have to call synchronize.

Take a look at this topic which goes through registering for the change notification on default changes. Nsuserdefaultschange
If multiple views need to check this value you could just expose the value as a property of your app delegate and check that property when needed. The app delegate would subscribe to the notification and reload the property value when it fires.

Related

NSUserDefaults - How should I fix this code?

I'm trying to make some sort of setup wizard that remembers a specific view however I have some problems with my AppDelegate.m code since I'm using ARC.
Does anyone know how to fix this, because If I compile the app crashes at the splash screen..
My AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSString *controllerName = [[NSUserDefaults standardUserDefaults] objectForKey:#"WIZARD_VIEW"];
if ([controllerName length]) {
Class controllerClass = NSClassFromString(controllerName);
UIViewController *controller = [[controllerClass alloc] init];
// Override point for customization after application launch.
return YES;
}
To be more clear, in the viewcontroller files I added the following code as a suggestion:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[NSUserDefaults standardUserDefaults] setObject:[[self class] description] forKey:#"WIZARD_VIEW"];
NSLog(#"ViewWillAppear Done.");
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"WIZARD_VIEW"];
NSLog(#"viewWillDisappear");
[[NSUserDefaults standardUserDefaults] synchronize];
}
Crashlog while compiling:
/Users/Tim/Documents/XCODE_DEV/App/AppDelegate.m:17:33: No visible #interface for 'NSUserDefaults' declares the selector 'forKey:'
/Users/Tim/Documents/XCODE_DEV/App/AppDelegate.m:20:27: Unused variable 'controller'
The first time you have to register the defaults.To register the defaults isn't enough to call the setObject:forKey method, you have to use the registerDefaults method:
- (void)registerDefaults:(NSDictionary *)dictionary;
Documentation:
NSUserDefaults
So in your case:
NSUserDefaults* standard=[NSUserDefaults standardUserDefaults];
[standard registerDefaults: #{ #"WIZARD_VIEW" : [[self class]description]} ];
You better place these lines in the initialize method, which gets called before any non-static method.If the defaults are already registred there's no problem, this will not overwrite the defaults that you have if is the 2nd+ time that the application starts.
Looking at the compiler warnings, I'm going to guess that in your AppDelegate.m, on line 17 you are calling forKey: on NSUserDefaults.
NSUserDefaults doesn't have a forKey: method, so it throws an exception.
It's probably just a square bracket in the wrong place.

UILabel becomes null after facebook login (UILabel dosen't update)

I am working on adding facebook SSO and the SDK to my project. All of my main facebook code is in my AppDelegate.m and AppDelegate.h.
In my view controller [[[UIApplication sharedApplication] delegate] performSelector:#selector(callLogin)]; is called when I press a button.
The callLogin method in my AppDelegate.m looks like this:
- (void)callLogin{
facebook = [[Facebook alloc] initWithAppId:#"XXXXX" andDelegate:self];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:#"FBAccessTokenKey"]
&& [defaults objectForKey:#"FBExpirationDateKey"]) {
facebook.accessToken = [defaults objectForKey:#"FBAccessTokenKey"];
facebook.expirationDate = [defaults objectForKey:#"FBExpirationDateKey"];
}
if (![facebook isSessionValid]) {
[facebook authorize:nil];
}
}
Then in my - (void)fbDidLogin method I call the method setInfo which is located in my viewcontroller.
//this method is located in AppDelegate.m
- (void)fbDidLogin {
NSLog(#"FACEBOOK DID LOGIN");
ViewController * vc = [[ViewController alloc]init];
[vc setInfo];
}
Finally, here is my -(void)setInfo code which is located in ViewController.m
-(void)setInfo{
infoL.text = [NSString stringWithFormat: #"Connected to Facebook!"];
NSLog(#"%#",infoL);
//NSLog returns null
}
From setInfo I am unable to change the label and NSLog returns that infoL is null. I can update the label through methods like ViewDidLoad, but not setInfo.
What am I doing wrong and how can I fix this?
The view controller won't get updated because you just created some random instance of the class that is not on your navigation stack. I suppose you could call
[ self.viewController setInfo ]
assuming that is the property name for the vc you pushed on the stack in applicationDidFinishLaunchingWithOptions although this wouldn't be considered great design. Id factor Facebook delegate stuff out out app delegate, create a Facebook controller singleton class that your view controller can feed off
I assume that infoL represents a UILabel. Do you create this label in code or is an outlet that is created automatically?
If you create infoL in code, where is it allocated/initted? Is it non-nil at any time before your setInfo method runs?

Application windows are expected to have a root view controller at the end of application launch

I am using the facebook iOS SDK setup tutorial: https://developers.facebook.com/docs/mobile/ios/build/
After Step 4: Adding Log Out to your App,
I get a blank white screen on the 5.1 simulator (xcode 4.3.2) and the console shows a message:
Application windows are expected to have a root view controller at the end of application launch
EDIT-1
Thanks for your responses;
I chose a "Single View Application" template while creating the app. In the MainStoryBoard.storyboard, I created an object and assigned the MyGreatIOSAppAppDelegate class to it. Drag-dropped the viewController outlet of this object to the View Controller.
here is the code in MyGreatIOSAppAppDelegate.m
#import "MyGreatIOSAppAppDelegate.h"
#import "xxxViewController.h"
#implementation IJSAppDelegate
#synthesize window = _window;
#synthesize viewController = _viewController;
#synthesize facebook;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
// Add the logout button
UIButton *logoutButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
logoutButton.frame = CGRectMake(40, 40, 200, 40);
[logoutButton setTitle:#"Log Out" forState:UIControlStateNormal];
[logoutButton addTarget:self action:#selector(logoutButtonClicked)
forControlEvents:UIControlEventTouchUpInside];
[self.viewController.view addSubview:logoutButton];
facebook = [[Facebook alloc] initWithAppId:#"id" andDelegate:self];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:#"FBAccessTokenKey"]
&& [defaults objectForKey:#"FBExpirationDateKey"]) {
facebook.accessToken = [defaults objectForKey:#"FBAccessTokenKey"];
facebook.expirationDate = [defaults objectForKey:#"FBExpirationDateKey"];
}
if (![facebook isSessionValid]) {
[facebook authorize:nil];
}
return YES;
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
return [facebook handleOpenURL:url];
}
- (void)fbDidLogin {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[facebook accessToken] forKey:#"FBAccessTokenKey"];
[defaults setObject:[facebook expirationDate] forKey:#"FBExpirationDateKey"];
[defaults synchronize];
}
// Method that gets called when the logout button is pressed
- (void) logoutButtonClicked:(id)sender {
[facebook logout];
}
- (void) fbDidLogout {
// Remove saved authorization information if it exists
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:#"FBAccessTokenKey"]) {
[defaults removeObjectForKey:#"FBAccessTokenKey"];
[defaults removeObjectForKey:#"FBExpirationDateKey"];
[defaults synchronize];
}
}
#end
Check that you have the following line in your application delegate's application:didFinishLaunchingWithOptions: method:
self.window.rootViewController = self.viewController;
Make sure you set your window.rootviewcontroller = your _navigationviewcontroller. Like this: (this all happens in your AppDelegate.cs file)
public partial class AppDelegate : UIApplicationDelegate
{
// class-level declarations
UIWindow _window;
UINavigationController _navigationcontroller;
//
// This method is invoked when the application has loaded and is ready to run. In this
// method you should instantiate the window, load the UI into it and then make the window
// visible.
//
// You have 17 seconds to return from this method, or iOS will terminate your application.
//
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
// create a new window instance based on the screen size
_window = new UIWindow (UIScreen.MainScreen.Bounds);
// If you have defined a view, add it here:
// window.AddSubview (navigationController.View);
_navigationcontroller = new UINavigationController();
_navigationcontroller.PushViewController(new SplashViewController(),false);
**_window.RootViewController = _navigationcontroller;**
// make the window visible
_window.MakeKeyAndVisible ();
return true;
}
}
}

How to run something once when the app is launched?

I'm trying to play an intro movie when my app is getting launch, but am totally driven crazy already, I did a lot of testing in my code, along with trying to use this
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
NSLog(#"App started to launch");
[self.window makeKeyAndVisible];
[self.window addSubview:_intro.view];
return YES;
}
but that's make my video run, even if I'm coming from the background.
If like pressing the middle button, then double pressing the middle button and pressing app icon, I get the movie to play again.
I forget to mention that this is my ViewDidLoad
- (void)viewDidLoad
{
[super viewDidLoad];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
BOOL launchedBefore = [userDefaults boolForKey:#"hasRunBefore"];
NSLog(#"value of hasRunBefore is %d",launchedBefore);
if(!launchedBefore)
{
[userDefaults setBool:1 forKey:#"hasRunBefore"];
launchedBefore = [userDefaults boolForKey:#"hasRunBefore"];
NSLog(#"value of hasRunBefore is %d",launchedBefore);
[self playvideo];
}
}
NSUserDefaults does not commit the changes to disk until you send the synchronize message.
Try adding doing this:
[userDefaults setBool:YES forKey:#"hasRunBefore"];
[userDefaults synchronize];

Load an other View from applicationDidFinishLaunching in the AppDelegate

I have this code in my applicationDidFinishLaunching in the AppDelegate:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *firsttime = [defaults stringForKey:#"firsttime"];
if (firsttime == nil) {
BenutzerdatenViewController *Benutzer = [[BenutzerdatenViewController alloc] initWithNibName:nil bundle:nil];
Benutzer.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[Benutzer release];
[defaults setObject:#"lasttime" forKey:#"firsttime"];}
else { [window addSubview:viewController.view];
[window makeKeyAndVisible];}
Always when I open the app the first time I just see a with "view" instead of the right view with buttons and so on. Where is the problem?
You need to add the actual view to your window. For firsttime, you need the following:
[window addSubview: Benutzer.view];
Also, don't release that viewController; you should store a reference to it somewhere.
In the other case (!firstime), it's unclear where you're getting viewController from; I assume it's a member variable.