Friday, February 24, 2017

Lightweight Migration Example

My existing app TasteBank is a personal taste tracking app.  It originally allowed you to enter a list of restaurants and track details about the dishes you ate at that restaurant.  I decided I wanted to make it a more generic taste tracker.  I wanted to add categories to the app so you could track your tastes not just in dishes at restaurants, but also your taste in wines, or coffee.

Here is the core data diagram for before:


And after:



So I basically added 1 new entity called Category and some new attributes.

According to apple docs, in order for me to merge these two different version of my data model, I would need to perform a lightweight migration.  Adding attributes or entities can usually be done using lightweight migrations.

I wanted to make sure that the existing data would not be overwritten and that the list of restaurants and dishes would just be moved to a category called Restaurants.

The following is a step by step list of what I had to do to complete this successfully.  There are many examples of the first few steps on the internet already, the last few steps are specific to my data.

Create a new database model version:

1. Click on my database .xcdatamodeld file in project navigator
2. Go to Editor in menu bar, and select Add Model Version...
3. Name the version of data model, I used TastebankVer2 and hit Finish
4. A little green arrow shows which version of the data model is currently being used, to make the new version the current active version, you click on the data model you just added, then go to utilities->Model Version->select current version in drop down list



Add code to enable lightweight migration:


1. Open App delegate file
2. Go to addPersistentStoreWithType:configuration:URL:options:error: method

Apple's suggested code shows:






NSError *error = nil;
NSURL *storeURL = <#The URL of a persistent store#>;
NSPersistentStoreCoordinator *psc = <#The coordinator#>;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
    [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
 
BOOL success = [psc addPersistentStoreWithType:<#Store type#>
                    configuration:<#Configuration or nil#> URL:storeURL
                    options:options error:&error];
if (!success) {
    // Handle the error.
}

Here is my persistentStoreCoordinator code:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }
    NSError *error = nil;

    NSURL *storeURL =                        
        [[self applicationDocumentsDirectoryURLByAppendingPathComponent:@"TasteB.sqlite"];
    
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:         [self managedObjectModel]];

    BOOL success = [ _persistentStoreCoordinator       
        addPersistentStoreWithType:NSSQLiteStoreType 
        configuration:nil URL:storeURL    
        options:@{NSMigratePersistentStoresAutomaticallyOption:@YES,      
        NSInferMappingModelAutomaticallyOption:@YES} error:&error];
        
    if (!success) {
    
         NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    
    return _persistentStoreCoordinator;
}

At this point the lightweight migration is activated and should work right away for adding attributes and stuff.

Copy old data into new structure

However I also wanted my existing data restructured.  I already had a list of restaurants and dishes in the old version of my app.

    ---------->      



When moving to the new version, I wanted all of this data moved under a new category called Restaurants, and I needed to update the category attribute on the Restaurant entities.
      ----->    ------>  

I put this code in my App delegate:

-(void)makeCategory{
    NSError *error = nil;
    
    NSFetchRequest *fetch = [[NSFetchRequest allocinitWithEntityName:@"Category"];
    
    NSUInteger numberOfcategories = [self.managedObjectContext countForFetchRequest:fetch        
        error:&error];
    
    //if there are no existing categories, then create a new one called Restaurants
    if( numberOfcategories == 0 ){
        NSLog(@"Categories not found, creating Restaurant Category");

        Category *c=[NSEntityDescription insertNewObjectForEntityForName:@"Category"    
            inManagedObjectContext:self.managedObjectContext];
        c.name=@"Restaurants";
        
        //if there are existing restaurants then set their category attribute to point to the new         //Restaurant category

        NSFetchRequest *fetch1 = [[NSFetchRequest allocinitWithEntityName:@"Restaurants"];
        if ([self.managedObjectContext countForFetchRequest:fetch1 error:&error] >0) {
            
            //make the new restaurant category relation

            NSArray *fetchedObjs= [self.managedObjectContext executeFetchRequest:fetch1 
               error:&error];
            for (Restaurants *r in fetchedObjs) {
                r.category=c;
                
            }
        
        }
    }
}

I called this function from didFinishLaunchingWithOptions: method . 


References:
https://the-nerd.be/2012/02/05/how_to_do_a_lightweight_core_data_migration/
https://www.objc.io/issues/4-core-data/core-data-migration/

Saturday, November 9, 2013

Renewing UILocalNotifications

(using iOS 7 XCode 5)



I am writing an app where I need to send the user a message at the same time everyday, indefinitely (unless the user cancels).   Alert messages can be scheduled with the OS using UILocalNotifications, for details on this check out the Apple documentation.  


It sounds simple enough: configure a local notification to appear every day at a time of the user’s choosing.  I could use the repeatInterval property which allows the app to schedule a daily alert.  But I want the user to receive a different message everyday and the repeatInterval property will show the same message body everyday.


So I have to schedule each alert separately each with a different message, and time.  Also note that there is a maximum number of notifications that can be scheduled at a given time (64 according to documentation).  So in order to schedule notifications that span over months either the app has to automatically keep adding notifications at a later date or the user has to manually reschedule them after 64 days.  I prefer the first solution where the app figures out that an alert has been shown and adds a new notification to the queue.  But for this to happen I would need a background process running even when app is not in the foreground.  According to my Googling, this is not allowed by the OS (to be certain I asked stackoverflow).  The second option seems cumbersome for the user.

My solution includes both of the options, having the user initiate the rescheduling by opening the app from the alert and having the app automatically reschedule when it is launched.  When an alert is shown, the action button says ‘go to app’ and it launches the app.  This ‘go to app’ action will take the user to the main page of the app, in the background, the app figures out how many local notification slots are available and then adds more notifications to the schedule.  This solution needs the user to click one button, and the app does the rest.  (When shown in a locked screen the user just needs to slide the alert and is taken to the app, again the app reschedules at that point.)  


I will break this up into two parts:
1. scheduling daily alerts (64 total)
2. adding to schedule in the background when app is launched.


Scheduling daily alerts



In my app I used a time picker to let the user choose the time of day they would like to receive their daily message and had them determine the content.  However for the sake of this example, let’s assume that the current time is used to schedule the notifications, and the alert messages are contained in an array of strings called userDefinedMessages.


Here is how I configure a local notification that repeats every day with a different alert message for 64 days:


//Create a calendar
NSCalendar *currentCalendar = [NSCalendar currentCalendar];


//Create a day component offset, days will be incremented by 1
NSDateComponents *dayComponent = [[NSDateComponents alloc] init] ;
[dayComponent setDay:1];


//Create date from current date so notifications start being scheduled from current time details here
NSDate *startDate =[NSDate date];


//Configure 64 local notifications, increment day component every time
for(int i =0;i<64;i++) {


UILocalNotification *localNotif = [[UILocalNotification alloc] init];
localNotif.timeZone = [NSTimeZone defaultTimeZone];
//content of the alert message are set from an array of messages set by user, not shown here
localNotif.alertBody = [userDefinedMessages objectAtIndex:i];
localNotif.alertAction = @”Go to App”;
//set the alert schedule date
startDate = [currentCalendar dateByAddingComponents:dayComponent toDate:startDate options:0];


localNotif.fireDate = startDate;
localNotif.soundName = UILocalNotificationDefaultSoundName;


[[UIApplication sharedApplication] scheduleLocalNotification:localNotif];


}
                   

Renewing Local Notifications



When a local notification fires, whether the alert launches the app or the app is already in the 
foreground, the didReceiveLocalNotification method will be called.  This can be added to the 
AppDelegate.m file.  In my case, when the app is launched from an alert, I check to see how 
many local notifications are left in the queue, if there is room, I add more.



- (void)application:(UIApplication *)application
didReceiveLocalNotification:(UILocalNotification *)notification {


//Get count of currently scheduled notifications, details here


int numberOfScheduledLocalNotifications = [[[UIApplication sharedApplication] scheduledLocalNotifications] count];
   
   int numberOfEmptyNotificationSlots = 64 - numberOfScheduledLocalNotifications;



//Sort the scheduledLocalNotifications array by firedate so that the last notification is
// first in the array.   This way we can find the last notification scheduled and add new notifications after that date. 
//This sorting code was obtained from here.


NSSortDescriptor *sortDes = [NSSortDescriptor sortDescriptorWithKey:@"fireDate" ascending:NO];

NSArray *notifArray = [[[UIApplication sharedApplication] scheduledLocalNotifications] sortedArrayUsingDescriptors:@[sortDes]];
   

//Get the date alert is scheduled for from the local notifications if there are any scheduled, otherwise just use 
//current date

   NSDate *fireDate;
   
   if([notifArray count]) {
       UILocalNotification *lastNotif=  [notifArray objectAtIndex:0];
       fireDate = lastNotif.fireDate;
   } else {
       fireDate=[NSDate date];
   }


//Create a calendar
   NSCalendar *currentCalendar = [NSCalendar currentCalendar];
  
   //Create a day component offset, days will be incremented by 1
   NSDateComponents *dayComponent = [[NSDateComponents alloc] init] ;
   [dayComponent setDay:1];
   
  //Configure local notifications, increment day component every time
for(int i =0;i<numberOfEmptyNotifiationSlots;i++) {


UILocalNotification *localNotif = [[UILocalNotification alloc] init];
localNotif.timeZone = [NSTimeZone defaultTimeZone];


//content of the alert message are set from an array of messages set by user, this array can be saved in core
//data then retrieved here or saved in NSUserDefaults
localNotif.alertBody = <get from user array>;


localNotif.alertAction = @”Go to App”;
//set the alert schedule date to be a day after the last scheduled notification
       fireDate = [currentCalendar dateByAddingComponents:dayComponent toDate:fireDate options:0];
       
       localNotif.fireDate = fireDate;
       localNotif.soundName = UILocalNotificationDefaultSoundName;
       
       [[UIApplication sharedApplication] scheduleLocalNotification:localNotif];
}
}
This will add more notifications to the schedule when the user launches the app from the alert 
message.  


Please note that there is a way to schedule notifications using the scheduledLocalNotification 
which I have not used here.
References: