Remind Me At – geofenced reminder tutorial, part 3

Remind Me At
We’re continuing development of Remind Me At, solution for very specific problem – create geofenced reminder by tapping on a map.

In this post we’ll create view controllers for reminder creation and display. And also, we’ll see how “unwind” segue works, and also – how we can avoid using it.

Let’s start with RMAddReminderViewController – view controller for adding geofenced reminders. Implementation is very simple – we’re using static table view. We’ve added outlets for controls we want to access.

@property (weak, nonatomic) IBOutlet UITextField *reminderTitleField;
@property (weak, nonatomic) IBOutlet UITableViewCell *reminderListCell;
@property (weak, nonatomic) IBOutlet UITableViewCell *reminderArrivalCell;
@property (weak, nonatomic) IBOutlet UITableViewCell *reminderDepartureCell;

Let’s see how we select EKCalendar where we’re going to add reminder. We won’t be creating our view controller for that. Instead, we’ll use solution from EventKitUI framework – EKCalendarChooser.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // ...
    if (indexPath.section == 1)
    {
        EKCalendarChooser *vc = [[EKCalendarChooser alloc] initWithSelectionStyle:EKCalendarChooserSelectionStyleSingle
                                                                     displayStyle:EKCalendarChooserDisplayWritableCalendarsOnly
                                                                       entityType:EKEntityTypeReminder
                                                                       eventStore:[RMReminderManager defaultManager].store];
        vc.delegate = self;
        vc.selectedCalendars = [NSSet setWithObject:self.calendar];
        [self.navigationController pushViewController:vc animated:YES];
    }
    // ...
}

-(void)calendarChooserSelectionDidChange:(EKCalendarChooser *)calendarChooser
{
    self.calendar = [calendarChooser.selectedCalendars anyObject];
    [self.navigationController popViewControllerAnimated:YES];
    [[NSUserDefaults standardUserDefaults] setObject:self.calendar.calendarIdentifier forKey:kDefaultCalendarKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

Since EKCalendarChooser is not created from storyboard, we’re not seguing to it, instead we’re using good old UITableViewDelegate‘s method -tableView:didSelectRowAtIndexPath:.

Our view controller is adopting EKCalendarChooserDelegate protocol, we’re implementing its method -calendarChooserSelectionDidChange:. There we get selected calendar (there will be only one object in the set) and dismiss chooser. We’re also storing calendar identifier in NSUserDefaults, so our view controller will remember last used calendar.

Now, let’s see how reminder is saved. This approach is not recommended for this particular case, it is done here only for demo purposes. We’ll use unwind segue.

How unwind segues work?

This is the only segue that is not instantiating new view controller. Instead, it dismisses one. To use unwind segues, you have to:

  1. Declare IBAction method with UIStoryboardSegue argument in the caller view controller;
  2. Link to this action in view controller you want to be dismissed.

Unwind segue could dismiss several view controllers at once. Method could be declared in any view controller higher up in hierarchy (controller A pushes B, B pushes C and C shows D modally; then D could be unwinded to A).

I’ll use our structure as an example. In RMMapViewController we declare -saveReminder: method.

-(IBAction)saveReminder:(UIStoryboardSegue*)segue
{
    // controller will be dismissed automatically
    RMAddViewController *vc = segue.sourceViewController;
    [[RMReminderManager defaultManager] addReminderWithAnnotation:vc.reminderAnnotation
                                         inCalendarWithIdentifier:vc.calendar.calendarIdentifier
                                                       completion:NULL];
    
    [self.mapView removeAnnotation:self.addedReminderAnnotation];
    self.addedReminderAnnotation = nil;
}

This method removes red pin from the map (because it would be replaced with green pin for existing reminder).

In storyboard we link Save button to this action by right-button-dragging from it to green “exit” button on view controller bar.
Unwind Segue Select
We’re also setting this segue an identifier. We’ll use it in -prepareForSegue: method.

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"SaveReminder"])
    {
        self.reminderAnnotation.title = self.reminderTitleField.text.length ? self.reminderTitleField.text :
                                                                              NSLocalizedString(@"Reminder", @"New reminder default title");
    }
}

When user taps on Save button, -prepareForSegue: method of RMAddReminderViewController is called, where we launch reminder creation process. Then -saveReminder: on RMMapViewController is executed. And only then RMAddReminderViewController is dismissed.

So, RMAddViewController is only preparing the data, while all operations with creation is performed in RMMapViewController. In better solutions this could be a good idea to use similar approach.

Another approach we’ll use in RMShowReminderViewController. This view controller shows information about geofenced reminder and also allows to delete one.

Here we’ll use blocks. In RMShowReminderViewController we define a completion handler – action to be performed when user decides to delete a reminder.

typedef void (^RMShowReminderViewControllerDeleteBlock)();

@interface RMShowReminderViewController : UITableViewController

@property (strong, nonatomic) EKReminder *reminder;
@property (copy, nonatomic) RMShowReminderViewControllerDeleteBlock deleteReminderCompletion;

@end

In -prepeareForSegue: of RMMapViewController we set this block to actually delete a reminder and dismiss RMShowViewController.

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"ShowReminder"])
    {
        RMShowReminderViewController *vc = segue.destinationViewController;
        MKAnnotationView *view = (MKAnnotationView *)sender;
        RMGeofencedReminderAnnotation*annotation = (RMGeofencedReminderAnnotation*)view.annotation;
        vc.reminder = annotation.reminder;
        
        vc.deleteReminderCompletion = ^{
            [[RMReminderManager defaultManager] removeReminder:annotation.reminder withCompletion:NULL];
            [self.navigationController popViewControllerAnimated:YES];
        };
    }
    // ...
}

This approach is simplified version of delegates. Instead of defining a protocol, implementing its methods, you can just set up your callback code when seguing view controller. Easy, convenient and yet effective. However, I wouldn’t vote against delegates, they’re also very simple and they’re used very widely in iOS SDK.

So, this wraps up our Remind Me At application. It solves our problem – we can tap anywhere on the map and set geofenced reminder there.

Sources of Remind Me At app are available on GitHub (use freely under terms of MIT License).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s