Omni Frameworks part 3: saving some data

This is a quickie, but I thought I’d write up a small stumbling block that I encountered. It probably came from my being relatively unacquainted with UIDocument, rather than an Omni-specific problem, but here goes.

I was implementing saving some data in the app I’ve been building up over part 1 and part 2 of this series. I made a map view, and decided that I would make the app save the user’s position as they panned the map view, so that when you opened the document again, it would be where you left it. 

After first being sure to link against MapKit, I then made a MapViewController. I gave it a mapView property and a document property, then put the following in loadView:

- (void)loadView
{
    MKMapView *mapView = [[MKMapView alloc] init];
    
	self.view = mapView;
	self.mapView = mapView;
	mapView.delegate = self;
}

Then I needed to get it on the screen. In LocusDocumentViewController (my OUIDocumentViewController-conforming class), in loadView, I created my MapViewController and added it to the view hierarchy:

    self.mapViewController = [[MapViewController alloc] init];
    self.mapViewController.document = self.document;
    self.mapViewController.view.frame = self.view.bounds;
    [self addChildViewController:self.mapViewController];
    [self.view addSubview:self.mapViewController.view];

Great, now we have a map on the screen when you open a document. So how do we go about saving the position? I added a property to LocusDocument (my OUIDocument subclass), of type MKCoordinateRegion, called mapRegion.

Then, in readFromURL:error: and writeContents:toURL:forSaveOperation:originalContentsURL:error:, I needed to actually load and save the data. Here’s the code I used:

- (BOOL)readFromURL:(NSURL *)url error:(NSError **)outError;
{
    NSData *modelData = [NSData dataWithContentsOfURL:url];
	
	NSKeyedUnarchiver *archiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:modelData];
	self.model = [archiver decodeObjectForKey:kModelKey];
    
    double lat = [archiver decodeDoubleForKey:kStopPickerRegionLat];
    double lon = [archiver decodeDoubleForKey:kStopPickerRegionLong];
    double latD = [archiver decodeDoubleForKey:kStopPickerRegionLatDelta];
    double lonD = [archiver decodeDoubleForKey:kStopPickerRegionLongDelta];
    self.mapRegion = MKCoordinateRegionMake(CLLocationCoordinate2DMake(lat, lon), MKCoordinateSpanMake(latD, lonD));
	
	return YES;
}

- (BOOL)writeContents:(id)contents toURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)saveOperation originalContentsURL:(NSURL *)originalContentsURL error:(NSError **)outError;
{
	NSMutableData *newData = [NSMutableData new];
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:newData];
	[archiver encodeObject:self.model forKey:kModelKey];
    
    [archiver encodeDouble:self.mapRegion.center.latitude forKey:kStopPickerRegionLat];
    [archiver encodeDouble:self.mapRegion.center.longitude forKey:kStopPickerRegionLong];
    [archiver encodeDouble:self.mapRegion.span.latitudeDelta forKey:kStopPickerRegionLatDelta];
    [archiver encodeDouble:self.mapRegion.span.longitudeDelta forKey:kStopPickerRegionLongDelta];
    
	[archiver finishEncoding];
	
	return [newData writeToURL:url atomically:NO];
}

The keys are all string constants that I defined at the top of the file. Essentially, to save an MKCoordinateRegion, I saved each of the four values that make it up.

At this point, whenever the document gets loaded and saved, it should save the mapRegion property of the document, and correctly restore it. How do we set that property? Back to the MapViewController:

I added a BOOL mapViewLoaded property, to make sure we only start saving the region once the map view has loaded. Then, in viewDidAppear, if the map view is not loaded I load the region out of the document. (I can get away with checking the latitude is not 0 because this app is targeted at users in Britain. A better design, and one which I might implement later, would be to ensure the document always has a mapRegion, by setting the default in LocusDocument’s initEmptyDocumentToBeSavedToURL:error: method.)

- (void)viewDidAppear:(BOOL)animated
{
    if (!self.mapViewLoaded)
    {
        if (self.document.mapRegion.center.latitude != 0)
        {
            [self.mapView setRegion:self.document.mapRegion animated:NO];
        }
        else
        {
            [self.mapView setRegion:MKCoordinateRegionMakeWithDistance(CLLocationCoordinate2DMake(53.47719, -2.2325), 1000, 1000)];
        }
        [self performSelector:@selector(loadPins:) withObject:self afterDelay:0.1];
    }

    self.mapViewLoaded = YES;
}

Then, in mapView:regionDidChangeAnimated:, I save the region to the document object:

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
    if (self.mapViewLoaded)
    {
        self.document.stopPickerMapRegion = mapView.region;
    }
}

That should be enough, right? It looks like we have all the pieces of the puzzle in place. At this point, I fired it up and… it didn’t work. Why not? It turned out that the saving code in LocusDocument did not seem to be getting called. 

At this point I did some digging into how UIDocument works. It turns out that you need to inform the system that the document has unsaved changes. I did this by overriding the setter for mapRegion:

- (void)setMapRegion:(MKCoordinateRegion)mapRegion
{
    _MpRegion = mapRegion;
    [self updateChangeCount:UIDocumentChangeDone];
}

And that was it. Now each document in the app shows a map view, that remembers its position and zoom settings.

Previous
Previous

Slides for my iOSDevUK talk on Templateable apps

Next
Next

Auto-boxing with performSelector:? Nope, but KVC works.