In my app, I create an EKCalendar. Since the calendar could be deleted by the user outside my app, I need to check if that calendar still exists, like so:
-(BOOL)checkForCalendar {
NSArray *calendarArray = [self.store calendarsForEntityType:EKEntityTypeEvent];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *calNameToCheckFor = [defaults objectForKey:#"calendarName"];
EKCalendar *cal;
for (int x = 0; x < [calendarArray count]; x++) {
cal = [calendarArray objectAtIndex:x];
NSString *calTitle = [cal title];
// if the calendar is found, return YES
if ([calTitle isEqualToString:calNameToCheckFor]) {
return YES;
}
}
return NO;
}
I want my UI to update, if a calendar indeed is deleted, as such:
-(void)initCalendarState {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if (![self checkForCalendar]){
[self.LoginSwitch setOn:NO animated:NO];
[userDefaults setObject:#"0" forKey:#"SwitchedOn"];
}
}
I have put this method in the Application's Delegate
- (void)applicationDidBecomeActive:(UIApplication *)application
Things I observe:
1) If I go through this code step by step in the debugger, it is called on the correct moment, and is executed as expected: running in the main thread, honoring all conditions etc.
2) However: when it gets to updating the UI, ([self.LoginSwitch setOn:NO animated:NO];), nothing happens.
3) When I rerun the project (effectively force quitting it, and restarting) the UI actually IS updated.
What am I missing?
The concrete question is: why isn't my UI updated while the app is running?
Thanks ahead
I think what is happening, is that the changes I made in my local calendar were not fully proliferated through the iCloud sync system. Or something like it. Not sure.
However: to solve my problem, I added
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(storeChanged:)
name:EKEventStoreChangedNotification
object:self.store];
to my app.
and
-(void)storeChanged:(NSNotification *) notification {
[self initCalendarState];
}
This works flawlessly.
Related
I want to run a piece of code -- for data cleanup -- only once after the user upgrades the app or installs the app from scratch.
Putting the piece of code in didFinishLaunchingWithOptions method wasn't helpful since -- it seems -- after the app is opened after the upgrade (without hard-killing it before the upgrade) the didFinishLaunchingWithOptions event wasn't triggered.
I can put the piece of code in the applicationWillEnterForeground event handler and have it conditioned on a dataCleanupDone flag being FALSE. Is there a neater solution?
After the clean up set a flag (kCleanUpCompletedKey) in the user preferences. Check if this flag is set when the app becomes active.
Example:
- (void)applicationDidBecomeActive:(UIApplication *)application {
if ([self shouldDoCleanUp]){
// do your clean up
// after successful clean up:
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setBool:YES forKey:kCleanUpCompletedKey];
}
}
- (BOOL)shouldDoCleanUp {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
if ([prefs objectForKey:kCleanUpCompletedKey]){
return YES;
}
return NO;
}
You need to add appversion in your new App version in preference and then
call method onUpgradingApp if prefrence value is 0 then set it to 1 and run this code
integerValue CurrentAppVersion = 2; // suppose
Inside Appdelegate :
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSInteger oldVersion = [[defaults objectForKey:#"appVersion"] integerValue];
if (oldVersion == 0) // if old app is running
{
oldVersion = 1;
}
if (oldVersion < CurrentAppVersion)
{
[self onUpgradingApp:oldVersion newVersion: CurrentAppVersion];
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInteger:CurrentAppVersion] forKey:#"appVersion"]; //save new version in preference
}
-(void)onUpgradingApp:(NSInteger)oldVersion newVersion:(NSInteger)newVersion
{
NSString *query;
switch (oldVersion)
{
case 1:
{
// clean data here
}
break;
default:
break;
}
}
SWIFT VERSION
you can use same logic.
1- Define your app version in the info.plist file
example: Bundle versions string, short: 0.1.1
And then you should increase that variable every time you have a new version.
2- in your controller create a variable app_version and get the right version from that info.plist file.
example: let version = dict["CFBundleShortVersionString"] as? String
3- Use the !UserDefaults.standards.bool to check if the app version changes, if true run your code and then set it to true
example:
if (!UserDefaults.standard.bool(forKey: appVersion)) {
/* Your code */
UserDefaults.standard.set(true, forKey: appVersion)
UserDefaults.standard.synchronize();
}
Hope this helps.
I am using the below code to save an int and some other stuff into NSUserDefaults from my AppDelegate. The other array that I'm saving is full of objects that conform to NSCoding, so I don't think that that's an issue. If the current place is not zero, then that means that there is a session in progress, so all the data is loaded. When saving my data, it only saves if the current place is not zero which indicates that there is a session in progress. I know this calls when I exit the app on my physical device because the NSLog message appears in the debugger.
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
if (self.currentPlace != 0) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *arr = self.places; // set value
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:arr];
[defaults setObject:data forKey:#"places"];
[defaults setInteger:self.currentPlace forKey:#"current"];
NSLog(#"Saving and Quitting b/c we have a place verified!");
[defaults synchronize];
}
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSInteger myInt = [defaults integerForKey:#"current"];
self.currentPlace = (int)myInt;
if (self.currentPlace != 0) {
NSData *data = [defaults objectForKey:#"places"];
NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:data];
self.places = [arr mutableCopy];
NSLog(#"Current Place: %i",self.currentPlace);
}
}
I am using my AppDelegate to store data that can be accessed from multiple screens in my app. In the first ViewController, the user is presented with a menu. If the appDelegate's currnentPlace value is not 0, then the option to continue with the loaded data from the AppDelegate is presented. However, this option is never presented. I check the currentPlace int's value using the following in my first view controller:
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
// Do any additional setup after loading the view, typically from a nib.
[self setupViews];
self.spinner.alpha = 0;
NSLog(#"Current %i",delegate.currentPlace);
if (delegate.currentPlace == 0) {
self.continueButton.enabled = false;
self.continueButton.alpha = 0.5;
}
else if (delegate.currentPlace != 0) {
self.continueButton.enabled = true;
self.continueButton.alpha = 1;
}
If anyone could see what I'm doing wrong it would be greatly appreciated.
Move your data restoration code block into a method, as below:
- (void)restoreData {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSInteger myInt = [defaults integerForKey:#"current"];
self.currentPlace = (int)myInt;
if (self.currentPlace != 0) {
NSData *data = [defaults objectForKey:#"places"];
NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:data];
self.places = [arr mutableCopy];
NSLog(#"Current Place: %i",self.currentPlace);
}
}
Then call this method in application:didFinishLaunchingWithOptions: and applicationWillEnterForeground: methods in AppDelegate.
You need to use
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
in you controller in which you are making the object of AppDelegate class.
If still you are not able to get current value then just store the value in user default as a string and when you acess it, convert it in appropriate data type which you want. Might be this could help you.
I have one question near the end.
I am working from the belief/experience that seeding iCloud more than once is a bad idea and that if a user can do the wrong thing, he probably will sooner or later.
What I want to do:
A. When the user changes the app preference "Enable iCloud" from NO to YES, display AlertView asking (Yes or No) if the user wishes to seed the cloud with existing non-iCloud Data.
B. Ensure that the app seeds iCloud only once on an iCloud account, refraining to put up the AlertView once seeding is completed the first time.
My Method:
Following Apple's Docs concerning the proper use of NSUbiquitousKeyValueStore, I am using the following method in, - (void)application: dFLWOptions:
- (void)updateKVStoreItems:(NSNotification*)notification {
// Get the list of keys that changed.
NSDictionary* userInfo = [notification userInfo];
NSNumber* reasonForChange = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
NSInteger reason = -1;
// If a reason could not be determined, do not update anything.
if (!reasonForChange)
return;
// Update only for changes from the server.
reason = [reasonForChange integerValue];
if ((reason == NSUbiquitousKeyValueStoreServerChange) ||
(reason == NSUbiquitousKeyValueStoreInitialSyncChange)) { // 0 || 1
// If something is changing externally, get the changes
// and update the corresponding keys locally.
NSArray* changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
// This loop assumes you are using the same key names in both
// the user defaults database and the iCloud key-value store
for (NSString* key in changedKeys) {//Only one key: #"iCloudSeeded" a BOOL
BOOL bValue = [store boolForKey:key];
id value = [store objectForKey:#"iCloudSeeded"];
[userDefaults setObject:value forKey:key];
}
}
}
Include the following code near the top of application: dFLWO:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(updateKVStoreItems:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:store]; // add appDelegate as observer
After loading iCloud Store, then seed it with non-iCloud data ONLY if seeding has never been done
- (BOOL)loadiCloudStore {
if (_iCloudStore) {return YES;} // Don’t load iCloud store if it’s already loaded
NSDictionary *options =
#{
NSMigratePersistentStoresAutomaticallyOption:#YES
,NSInferMappingModelAutomaticallyOption:#YES
,NSPersistentStoreUbiquitousContentNameKey:#"MainStore"
};
NSError *error=nil;
_iCloudStore = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil URL:[self iCloudStoreURL] options:options error:&error];
if (_iCloudStore) {
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded =
[store boolForKey:#"iCloudSeeded"];//If the key was not found, this method returns NO.
if(!iCloudSeeded) // CONTROL IS HERE
[self confirmMergeWithiCloud]; // Accept one USER confirmation for seeding in AlertView ONCE world wide
return YES; // iCloud store loaded.
}
NSLog(#"** FAILED to configure the iCloud Store : %# **", error);
return NO;
}
Once the seeding is completed do the following to prevent any repeat seeding:
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloud];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"]; // NEVER AGAIN
//[store synchronize];
}
}
}
Be sure to get a total iCloud reset before the above process using:
[NSPersistentStoreCoordinator
removeUbiquitousContentAndPersistentStoreAtURL:[_iCloudStore URL]
options:options
error:&error])
This is a very tidy solution to my problem, IMHO, but I can not quite get it done.
MY QUESTION:
How do I respond to the first notification to updateKVStoreItems: above? It is a notification with bad info. I says the value is TRUE, but I have never set it to TRUE. How do I set default values for a key in NSUbiquitousKeyValueStore?
I find that the first notification is of reason : NSUbiquitousKeyValueStoreInitialSyncChange
When that note comes in, bValue is YES. THIS IS MY PROBLEM. It is as if, iCloud/iOS assumes any new BOOL to be TRUE.
I need this value to be NO initially so that I can go ahead and follow the Apple Docs and set
the NSUserDefault to NO. And then Later when the seeding is done, to finally set the value: YES for the key:#"iCloudSeeded"
I find I can not penetrate the meaning of the following from Apple:
NSUbiquitousKeyValueStoreInitialSyncChange
Your attempt to write to key-value storage was discarded because an initial download from iCloud has not yet happened.
That is, before you can first write key-value data, the system must ensure that your app’s local, on-disk cache matches the truth in iCloud.
Initial downloads happen the first time a device is connected to an iCloud account, and when a user switches their primary iCloud account.
I don't quite understand the implications of number 2 below, which I found online:
NSUbiquitousKeyValueStoreInitialSyncChange – slightly more complicated, only happens under these circumstances:
1. You start the app and call synchronize
2. Before iOS has chance to pull down the latest values from iCloud you make some changes.
3. iOS gets the changes from iCloud.
If this problem was with NSUserDefaults and not NSUbiquitousKeyValueStore, I believe I would need to go to registerDefaults.
I am almost there,
How do I do this please!
Thanks for reading, Mark
The code was looking for both
A. NSUbiquitousKeyValueStoreInitialSyncChange and
B. NSUbiquitousKeyValueStoreServerChange
I was unable to figure out what to do with the notifications. I know see that I did not need to do anything with either. My app only needs to read and write, in order to solve the problem I laid out in my question header.
The app gets the current value with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded = [store boolForKey:#"iCloudSeeded"];
The app sets the value in the NSUbiquitousKeyValueStore with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"];
I believe I am correct in saying the following: Writing is done into memory. Very soon thereafter the data is put by the system onto disk.
From there it is taken and put into iCloud and is made available to the other devices running the same app on the same iCloud account. In the application I have described, no observer needs to be added, and
nothing else needs to be done. This is maybe an "unusual" use of NSUbiquitousKeyValueStore.
If you came here looking for a an more "usual" use, say when a user type something into a textview and it later
appears on a view of other devices running the same app, check out a simple demo I came across at :
https://github.com/cgreening/CMGCloudSyncTest
The better functioning (monitoring only) notification handler follows:
- (void)updateKVStoreItems:(NSNotification*)notification {
NSNumber *reason = notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
if(!reason) return;
// get the reason code
NSInteger reasonCode = [notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] intValue];
BOOL bValue;
NSUbiquitousKeyValueStore *store;
switch(reasonCode) {
case NSUbiquitousKeyValueStoreServerChange:{ // code 0, monitoring only
store = [NSUbiquitousKeyValueStore defaultStore];
bValue = [store boolForKey:#"iCloudSeeded"];
id value = [store objectForKey:#"iCloudSeeded"];
DLog(#"New value for iCloudSeeded=%d\nNo Action need be take.",bValue);
// For monitoring set in UserDefaults
[[NSUserDefaults standardUserDefaults] setObject:value forKey:#"iCloudSeeded"];
break;
}
case NSUbiquitousKeyValueStoreAccountChange: {// ignore, log
NSLog(#"NSUbiquitousKeyValueStoreAccountChange");
break;
}
case NSUbiquitousKeyValueStoreInitialSyncChange:{ // ignore, log
NSLog(#"NSUbiquitousKeyValueStoreInitialSyncChange");
break;
}
case NSUbiquitousKeyValueStoreQuotaViolationChange:{ // ignore, log
NSLog(#"Run out of space!");
break;
}
}
}
Adding 9/3/14
So sorry but I continued to have trouble using a BOOL, I switched to an NSString and now
all is well.
METHOD TO ENSURE THAT THE "MERGE" BUTTON FOR SEEDING ICOUD IS USED AT MOST ONCE DURING APP LIFETIME
Use NSString and not BOOL in KV_STORE. No need to add observer, except for learning
In Constants.h :
#define SEEDED_ICLOUD_MSG #"Have Seeded iCloud"
#define ICLOUD_SEEDED_KEY #"iCloudSeeded"
Before calling function to seed iCloud with non-iCloud data:
NSUbiquitousKeyValueStore* kvStore = [NSUbiquitousKeyValueStore defaultStore];
NSString* strMergeDataWithiCloudDone =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
NSComparisonResult *result = [strMergeDataWithiCloudDone compare:SEEDED_ICLOUD_MSG];
if(result != NSOrderedSame)
//put up UIAlert asking user if seeding is desired.
If user chooses YES : set Value for Key after the merge is done.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloudwithNoniCloudData];
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
[store setObject:SEEDED_ICLOUD_MSG forKey:ICLOUD_SEEDED_KEY];
}
}
}
Thereafter on all devices, for all time, the code
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
NSString* msg =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
produces: msg == SEEDED_ICLOUD_MESSAGE
Is it possible to save and load data on Today Extension using NSUserDefaults?
After closing the Notification Center, the widget behaves like an app which is terminated, so any data results lost. How could I solve this issue?
This is my code:
NSUserDefaults *defaults;
- (void)viewDidLoad {
[super viewDidLoad];
defaults = [NSUserDefaults standardUserDefaults];
NSArray *loadStrings = [defaults stringArrayForKey:#"savedStrings"];
if ([loadStrings objectAtIndex:0] != nil) {
[display setText:[NSString stringWithFormat:#"%#", [loadStrings objectAtIndex:0]]];
}
if ([loadStrings objectAtIndex:1] != nil) {
calculatorMemory = [NSString stringWithFormat:#"%#", [loadStrings objectAtIndex:1]].doubleValue;
}
}
- (IBAction)saveData:(id)sender {
NSString *displayString;
NSString *memoryString;
NSArray *saveStrings = [[NSArray alloc] initWithObjects: displayString, memoryString, nil];
defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:saveStrings forKey:#"savedStrings"];
[defaults synchronize];
}
You need to use app group identifier instead of com.*
For instance:
NSUserDefaults *shared = [[NSUserDefaults alloc]initWithSuiteName:#"group.company.appgroup"];
Don't forget to synchronise when you store data
[shared synchronize];
You need to add the App Group stuff detailed under here and then if it actually worked (pretty iffy under beta) it should allow you to share NSUserDefault data like normal between the host and widget.
Edit: Normal NSUserDefaults does not work. Apple has implemented a new method. To use, simply redefine your NSUserDefaults instance like this:
NSUserDefaults *shared = [[NSUserDefaults alloc]initWithSuiteName:#"com.you.app.container"];
For anyone wondering how in the world do you save and get values then look at this code.
In your regular app add this to save whatever you like in your *.m file.
NSUserDefaults *shared = [[NSUserDefaults alloc]initWithSuiteName:#"group.yourcompanyname.TodayExtensionSharingDefaults"];
//save dic
[shared setObject:dictionary2 forKey:#"dicForTodayWidget"];
//save array
[shared setObject:tempArray2 forKey:#"arrayForTodayWidget"];
//save some value
[shared setObject:#"1234" forKey:#"myValForTodayWidget"];
[shared synchronize];
In your today widget under TodayViewController.m in viewDidLoad add this.
NSUserDefaults *shared = [[NSUserDefaults alloc]initWithSuiteName:#"group.yourcompanyname.TodayExtensionSharingDefaults"];
//get dic
NSMutableDictionary *dictionary = [shared objectForKey:#"dicForTodayWidget"];
You first need the App Groups set up for both targets (application and the extension).
Then, use the
NSUserDefaults *shared = [[NSUserDefaults alloc]initWithSuiteName:#"group.company.myapp"];
to obtain the defaults object which you can read from/write to as usual.
If you want to be notified of changes to the defaults, use the NSUserDefaultsDidChangeNotification in your widget (or app).
For a step-by-step tutorial explaining all this, take a look at this blog post.
#edukulele
Today Extension and Main app run on two processes. Today Extension can't receive NSUserDefaultsDidChangeNotifications. I tried use MMWormhole. It is very good.
I am about to release an iPhone app for a small business. The app has a call button which calls the business with an ibaction. I would like to know if i can find out how many times this action occurs so i can get a better idea of how effective the app is, and how to go about it.
Thanks for your time.
You could implement a web-service with a simple method which will increment a counter on a server. And call it asynchronously every time user taps a button. I would do it like this:
#define TAP_COUNT_KEY #"tapcnt"
#define MAKE_URL(X) [NSURL URLWithString:[NSString stringWithFormat:#"http://my_service_address?cnt=%d", X]]
dispatch_queue_t serviceQueue = dispatch_queue_create("Tap counter queue", NULL);
dispatch_async(serviceQueue, ^{
NSUInteger counter = [[NSUserDefaults standardUserDefaults] integerForKey:TAP_COUNT_KEY] + 1;
NSURL *serviceUrl = MAKE_URL(counter);
NSError *error = nil;
[NSString stringWithContentsOfURL:serviceUrl encoding:NSUTF8StringEncoding error:&error];
if (error)
NSLog(#"%#", [error localizedDescription]);
else
counter = 0;
[[NSUserDefaults standardUserDefaults] setInteger:counter forKey:TAP_COUNT_KEY];
});
dispatch_release(serviceQueue);
On IBAction method, you can increment counter by +1. Then you can store it into NSUserDefaults by key-value pair. And every time, increase counter and store it. So it'll maintain counter state until application will be uninstalled.
int counter;
-(IBAction)click:(id)sender
{
NSUserDefaults *_countDefaults = [NSUserDefaults standardUserDefaults];
counter = [[_countDefaults integerForKey:#"Counter"] intValue];
counter++;
[_countDefaults setInteger:counter forKey:#"Counter"];
[_countDefaults synchronize];
}
Hopefully, you'll get idea.
Thanks.
You can increase your button tag value and access it where ever you want,
-(IBAction)btnPanic:(id)sender
{
UIButton *btn=(UIButton *)sender;
btn.tag=btn.tag+1;
}