Amy Worrall
  1. Using auto-layout to calculate table cell height

    5th November 2013

    Table cell height is one of the tricky bits of UITableView. You have to calculate it manually, in advance of creating your cell. Here’s a method that uses auto-layout to help you calculate it.

    First of all, you’ll need a cell with its content laid out with auto-layout. The easiest way to make one is to use a xib file. Make a new xib file with only one object (a table cell) inside. Add whatever subviews you want to your table cell, and lay them out with auto-layout.

    image

    Now, I’m not an auto-layout master. I’ve seldom used it before. So it took a bit of trial and error to get the layout set up correctly. The crucial thing was to make sure there were no constraints limiting the height of my labels, since I want them to resize themselves to fit whatever text goes in them.

    I set up tags for my labels, so I can refer to them in code in order to populate them. I also made sure to set the labels to support multiple lines of text, by setting numberOfLines to 0 and lineBreakMode to word wrap.

    Next, in your UITableViewController subclass, you need to keep a prototype UITableViewCell:

    @property (nonatomic, strong) UITableViewCell *prototypeCell;

    In the UITableViewController subclass’s init method, I made sure to register my cell’s nib:

    self.cellNib = [UINib nibWithNibName:@"MyCustomCell" bundle:nil];
    [self.tableView registerNib:self.cellNib forCellReuseIdentifier:@"CustomCell"];

    In your heightForRowAtIndexPath method, create this cell if it doesn’t already exist, then populate it with data.

    - (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    	 if (!self.prototypeCell)
    	 {
    		 self.prototypeCell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];
    	 }
    	
    	[self configureCell:self.prototypeCell forIndexPath:indexPath isForOffscreenUse:YES];
    	
    	[self.prototypeCell layoutIfNeeded];
    	CGSize size = [self.prototypeCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    	return size.height;
    }

    In this method, I’m creating a cell by dequeuing one from the table view, then calling a method to customise the cell. I do the same thing in cellForRowAtIndexPath:

    - (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    	UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];;
    	[self configureCell:cell forIndexPath:indexPath isForOffscreenUse:NO];
    	
    	return cell;
    }
    

    Looking back at heightForRowAtIndexPath:, what’s going on after we have a prototype cell?

    First we tell out cell to layoutIfNeeded. Then we ask it for systemLayoutSizeFittingSize:UILayoutFittingCompressedSize. This means we’re asking the layout system for the smallest size possible that will contain all the content.

    So what does this configureCell method do? Whatever you want: it’s the method that applies the data to the cell. The important thing is, for the most part it doesn’t matter to that method whether it’s configuring the prototype cell used for height calculation, or configuring an actual cell that will be put on screen. For the few occasions where it does matter, I’ve added a forOffscreenUse: flag. (The only time I used the flag is when setting up key-value observing: I never want to KVO the prototype cell.)

    There’s one more thing: due to what I think is a bug, I had to subclass UILabel and add this method:

    - (CGSize) intrinsicContentSize
    {
        CGSize s = [super intrinsicContentSize];
    	
        if ( self.numberOfLines == 0 )
        {
            // found out that sometimes intrinsicContentSize is 1pt too short!
            s.height += 1;
        }
    	
        return s;
    }
    

    And that’s it: cell heights should configure themselves to fit your content.

    Note that this method isn’t that efficient. If you’re using a large table view, you’ll definitely want to implement tableView:estimatedHeightForRowAtIndexPath:. This method lets you provide a rough guess at a row height, without needing to be so accurate. You could even just return a constant number, based on the average height of your cells. If you don’t implement this method, then before it displays anything, UITableView will go through every row in your table, apply its data to the prototype cell and ask auto-layout to calculate a height, which can be very slow if there are hundreds of rows.

    For more reading, have a look at this StackOverflow question.

  2. Predicting an Apple event

    17th October 2013

    image

    It’s prediction time again: Apple have sent an invitation for their October event. I just read Nick Heer’s predictions for what we’ll see. The thing that struck me was how many of them there are. From my memory of Apple announcement, I didn’t think they’d announce so many things at once. So I went digging.

    Apple’s events usually have one headline hardware announcement: the one thing that they want you to remember afterwards. This may comprise a few products (iPhone 5S and 5C, for example), but they’ll be related products that together tell one story. Then there may be one unrelated minor announcement, an update regarding a new OS version (that has already been revealed), plus one or two software or service announcements, and some mention of new accessories. And that’s usually it, isn’t it?

    Let’s look at examples, starting with the iPhone 5 launch. The main announcement was of course the 5. The minor but unrelated hardware was the new Nano. The OS update was iOS 6, which had already been previewed at WWDC. The software/service was iTunes 11. Accessories were new earbuds and lightning adaptors. This fits the pattern.

    The iPhone 5S/5C event: the one we’ve just had. Headline was the new iPhones. OS update was iOS 7, which again we’d seen at WWDC. Service was iTunes Radio. There was no minor announcement (I guess it could be argued that the 5S was the headline and the 5C was the minor announcement). Accessories included 5C cases. This one also fits the pattern.

    The iPad mini event: the big news was the iPad mini, plus the related but less important announcement of the iPad 4. Minor but unrelated hardware? Well, we had the new thin iMacs, new retina Macbook Pro 13”, spec bumps for the Mac mini, and the Fusion Drive technology. Software-wise we got iBooks and iBooks Author updates. So this announcement doesn’t quite fit the pattern — the iPad mini was the take-home message, but the new thin iMacs were also a big deal.

    So what of the upcoming event? The headline feature would be iPads. We’ve had lots of leaks for the new full-size iPad, with its narrower bezel and smaller size, so I think that’s a cert. iPad mini is also due for an upgrade, although I couldn’t call whether it will go retina or not. The minor unrelated hardware would be Mac Pro — minor in that it’s been previewed already, so it’s not a new headliner. OS update will be Mavericks, which has also already been seen. So that’s all of Nick’s “Count on” section (apart from iOS 7 update: I doubt they’d take up any presentation time showing one, even if one does come out around then.)

    What about his sandwich, coffee or nickel sections? Well, we don’t have a software or service release yet, so iWork could go there. I reckon we’re more likely to see updated iLife though, especially with the leaked icons. Other apps: iBooks might get a mention, if the update is significant. A Find my Friends update might, mainly because it was the poster child for skeumorphism. Whether or not they’re talked about at the event, I’d expect updates for everything except maybe Remote to happen by the end of the year. (Remote, as far as I can tell, is someone’s pet project rather than something Apple is committed to.)

    Dropping iPad 2 isn’t an announcement thing — it may well happen, but they won’t mention it. Ditto dropping the iPod Classic, which I’m surprised hasn’t happened already. (I did notice one prominently on display in an Apple Store recently though, which surprised me.) Updated MacBooks, if they’re just a spec bump, could fit into this event even alongside the Mac Pro, based on the precedent set at the iPad mini event.

    The new thunderbolt display is an interesting one. All signs point to Apple attempting to build a super high resolution display: especially the new Thunderbolt 2, specifically stated as driving 4K displays. And also, if it’s not super high res, then why make a new one? But launching that, a very much pro feature and one that would grab headlines, in the same event as the consumer iPads, seems odd. I reckon if they have a new display, then both that and the Mac Pro release will get their own pro-focussed event, maybe also incorporating a Final Cut Pro update.

    I agree that the smartwatch is a “no chance”, and anything to do with TV is unlikely (much as I’d love an SDK for the Apple TV, to go with the new game controller support in iOS…).

    To conclude, it is entirely possible to get a long list of updated things into one Apple event. When making predictions, it’s important to think of which ones are the headline grabbers: if a smartwatch did come out, for example, then that would have to be the headline product and the iPads would be regulated to the “minor update” part of the presentation. But Apple do sometimes get a huge list of updates into one event, even if the audience are only intended to remember one or two of them.

  3. Slides for my iOSDevUK talk on Templateable apps

    10th September 2013

    At iOSDevUK last week, I gave a talk on making templateable apps — that is, apps with a single codebase but different content, theming or features. As promised, I’m sharing the slides:

    Download the PDF of my slides

    I’ll hopefully blog in more detail on this topic in the future (perhaps including sharing a reusable class for reading the config plists). If I haven’t done it by October, email me to remind me!

  4. Omni Frameworks part 3: saving some data

    25th August 2013

    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.

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

    20th August 2013

    While trying to debug some code I encountered, I came across this article by Marcus Zarra: Does Objective-C Perform Autoboxing on Primitives? 

    The article has been retracted. It initially stated that when calling performSelector:withObject: on a method that took a primitive argument, you could pass in an NSNumber and Cocoa would unbox it for you. However, it turns out that the trick does not in fact work, so the article comes with a big disclaimer at the top.

    If you’re ever in that situation, where you need to call a method that takes a primitive argument indirectly, what should you do? There are a number of ways, such as using NSInvocation, or working with the method’s IMP directly, both of which I may talk about further in another article. There’s one more way though: humble Key Value Coding.

    It turns out KVC does perform auto-unboxing for you. So if the method you are trying to call meets the criteria for a KVC method (e.g. it is in the form setSomething:,), you can do this:

    - (void)start;
    {
        [self setValue:@(1415) forKey:@"aThingy"];
    }
    
    - (void)setAThingy:(int)anInt;
    {
        NSLog(@"An int: %d", anInt);
    }

    This will work with some other primitive types too, including BOOLs, floats, and even structs wrapped in an NSValue. The complete list is found in Apple’s Key Value Coding documentation.

  6. Threads — an idea for an App.net client

    19th August 2013

    For those of you who don’t know, App.net is like Twitter but for money. No, wait, that’s not how we’re supposed to introduce it — it does other stuff too, like file hosting and chat rooms. And there are free accounts now, so it’s not only a paid service. Hmm, let’s try that again…

    App.net is a service where users can post short textual messages, which will show up on the stream of any user who follows them. A message can have some annotations — either standard ones, like location or the app they posted with, or custom ones defined by an app author. Messages can also be posted to channels, which are like private rooms for particular participants. So even if the starting point was something like Twitter, App.net is more extensible by developers and can do more things.

    One feature I like about App.net is how well it handles replies. On Twitter, a post can only count as a reply if it contains the @username of the person whose post you are replying to. Not so in App.net — you can even reply to a post without mentioning any usernames, and the service will track the whole conversation in order so that users can read it from the beginning.

    I’ve had a design in mind for an App.net client that I’ve been calling Threads. It’s based on web forums, so I guess it could be a web app. I’m posting about it here because in all seriousness, I’m not going to have the time to write it in the near future, so if someone else wants to take this idea and run with it, be my guest.

    The main view of the app shows your feed, but organised by thread. That is, it lists all the posts in your feed that are not replies to another post. They’re listed in the order of their latest reply (most recent at the top), so if someone replies to a conversation, the opening post for that conversation jumps to the top of the list. The number of replies in the conversation are prominently shown next to the post, as is the username of the most recent replier.

    Click on a post, and you get to view the thread. It shows you all the posts in the thread, in chronological order, oldest at the top, just like a web forum. I’d also imagine it would remember which posts in the thread you had already read, and make their background colour look faded or something, so when you come back to a thread later you can find your place.

    For each post in the thread, if it’s in reply to the post immediately above it, that’s fine and not confusing. But what if it’s in reply to a post from further back? In that situation, the parent post is quoted inline, in a smaller font, like how the Quote button works on most web forums. This happens automatically.

    Messages that are posted through Threads can have a bit of extra metadata. For example, if you start a new thread (i.e. make a post that is not a reply), it’ll let you type a subject as well as the body text. This subject will be saved in a private annotation, so that Threads (and any other client that cares to) can display that when displaying the list of threads, rather than having to display the text of the original post.

    A reply posted through Threads can let the user choose which bit of text (if any) to quote. Remember that when displaying normal messages, it quotes no text if the reply is to the last message in the thread, or it quotes the full text if it’s a reply to an earlier message? Well, if you reply through Threads, you can override it in either direction, or even quote a subset of the message. Again, the quote text is stored in an annotation.

    A user would be able to flag favourite threads in order to follow them, and there would be a page showing all the flagged threads. Another would list all the threads you posted in. Any settings, such as flagged threads, would be saved in your ADN account.

    When viewing the list of threads, you could choose to view just threads started by people you follow, or all threads participated in by people you follow (even if started by someone else). 

    That’s about it. I have a few more ideas, but this is the general gist of it. Again, if anyone wants to make this, feel free (just let me know!).

    Update: I’ve just been clued in to TreeView, which does a great job at displaying a thread. (It displays it hierarchically, rather than chronologically with quoting like I described, but that’s still not a bad way to view them.) If only they’d add the thread list page!

  7. Omni Frameworks Part 2: Using the Document Picker

    26th July 2013

    Here’s part two of a series of posts chronicling my experiences using the Omni Frameworks. As before, I’d like to add the disclaimer that I don’t know if what I’ve done is best practice: the Omni Frameworks are not documented and I’m figuring things out as I go along.

    Human readable Copyright

    Here’s one tip for you: the Omni Frameworks seem to parse the copyright string in Info.plist. I got a few crashes where an assert failed. (Strangely this didn’t happen every time.) Make sure you have a human readable copyright string. This one worked for me:

    image

    Document types

    We’re going to be building a document picker. This lets your users pick a document to edit, much like in Apple’s iWork apps. So we’re going to need to declare some document formats that the app supports. These settings are also in the Info tab. Here’s mine:

    image

    First I defined a document type. I gave it a name, and a UTI. Now, this UTI was for a custom document, so I made it up. If you wanted to support a standard document type like RTF or PDF, you’d need to use the UTI for that type.

    I added the CFBundleTypeRole and LSHandlerRank keys because Omni’s sample had them in. I’m not sure what they’re for.

    Next I had to declare the UTI. I put it in Exported UTIs because it’s one I made up. If it was an existing UTI, I’d put this information in Imported UTIs instead.

    I added the same Description and Identifier. Then I made a dictionary key, UTTypeTagSpecification, under the additional properties section. In it I put public.mime-type (which again I made up), public.filename-extension (which is obviously the desired filename extension), and com.apple.ostype, which is a HFS Type Code. Type codes are four character strings that represented file types on the old MacOS. Use upper and lower case letters — ones that are all lower case are reserved by Apple. Again, I made this one up.

    Creating new documents

    We need to do a couple of main things to get the document picker to work. We’ll need a document class, to represent the file on disk. We’ll need a view controller that’ll handle displaying that document. And we’ll need some modifications to the App Delegate to tell it what types of document to create.

    Start by making the two classes I mentioned. I made LocusDocument (inherits from OUIDocument), and LocusDocumentViewController (inherits from OUIViewController and conforms to OUIDocumentViewController).

    Now, in the App Delegate, we can tell it what to create:

    - (Class)documentClassForURL:(NSURL *)url;
    {
        return [LocusDocument class];
    }
    
    - (NSString *)documentStoreDocumentTypeForNewFiles:(OFSDocumentStore *)store;
    {
        return @"com.locusapp.locus";
    }

    In the LocusDocument class, we then need to tell it how to make its view controller:

    - (UIViewController *)makeViewController;
    {
        return [[LocusDocumentViewController alloc] init];
    }
    

    There are a couple of other methods to implement here. It needs to know how to read and write its contents to a file. For now, I’ll put stub methods in:

    - (BOOL)readFromURL:(NSURL *)url error:(NSError **)outError;
    {
        return YES;
    }
    
    - (BOOL)writeContents:(id)contents toURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)saveOperation originalContentsURL:(NSURL *)originalContentsURL error:(NSError **)outError;
    {
        NSData *newData = [NSData data];
    	return [newData writeToURL:url atomically:NO];
    }
    

    Next, there are a couple of methods related to previews (i.e. the little thumbnails displayed in the document picker). This is code I copied out of Omni’s sample app with only minor changes: I’ve put it in a GitHub gist to save pasting lots of code here.

    One more thing to mention: if you want to set up anything when a new document is created, override this method:

    - (id)initEmptyDocumentToBeSavedToURL:(NSURL *)url error:(NSError **)outError;

    The View Controller

    Here’s what I put in the header file for LocusDocumentViewController:

    @interface LocusDocumentViewController : OUIViewController<OUIDocumentViewController>
    
    @property(nonatomic) BOOL forPreviewGeneration;
    
    @property (nonatomic, weak) LocusDocument *locusDocument;
    @property (nonatomic, strong)  UIToolbar *toolbar;
    
    @end
    

    I gave it three properties. First, a BOOL to show whether this view controller was created just in order to generate a preview thumbnail. This is needed later when we come to add a toolbar. Secondly a weak property that refers back to the document. Thirdly, a UIToolbar.

    In the code for the view controller, we firstly need to deal with the fact that the OUIDocumentViewController protocol declares a property called document. We shouldn’t synthesize it though, just implement these methods:

    - (OUIDocument *)document
    {
        return self.locusDocument;
    }
    
    - (void)setDocument:(OUIDocument *)document
    {
    	self.locusDocument = (LocusDocument*)document;
    }

    All I’m doing there is assigning to and reading from the locusDocument property instead. The point of that is so that I can access it inside the class without having to cast all the time.

    We also need to make sure the view controller uses the same undo manager as the document:

    - (NSUndoManager *)undoManager;
    {
        return [self.locusDocument undoManager];
    }
    

    Now let’s make it display something:

    - (void)loadView
    {
        [super loadView];
    	self.view.backgroundColor = [UIColor redColor];
    } 

    Finally in your App Delegate, implement:

    - (UIView *)pickerAnimationViewForTarget:(OUIDocument *)document;
    {
        return ((LocusDocumentViewController *)document.viewController).view;
    }
    

    That should be enough to get something on the screen. At this point, I would expect that you can use the document picker’s Add button to create a new document, and that tapping that document will take you to a screen that is coloured in red.

    Of course, from that screen you can’t get back again. For that you need:

    Implementing the toolbar

    Back in your App Delegate, declare the following instance variable:

    @implementation AppDelegate
    {
        NSArray *_documentToolbarItems;
    }
    

    Then add this method to the App Delegate, which is used to create a set of toolbar items:

    - (NSArray *)toolbarItemsForDocument:(OUIDocument *)document;
    {
        if (!_documentToolbarItems) {
            NSMutableArray *items = [NSMutableArray array];
            
            [items addObject:self.closeDocumentBarButtonItem];
            
            [items addObject:self.undoBarButtonItem];
            
            [items addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL] ];
            
            UIBarButtonItem *omniPresenceBarButtonItem = [self.document omniPresenceBarButtonItem];
            if (omniPresenceBarButtonItem != nil)
                [items addObject:omniPresenceBarButtonItem];
        	
            [items addObject:self.documentTitleToolbarItem];
            
            [items addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL] ];
            
            _documentToolbarItems = [[NSArray alloc] initWithArray:items];
        }
        
        return _documentToolbarItems;
    }
    

    Now we need to actually create and hook up a toolbar. We do this in the LocusDocumentViewController class:

    - (void)loadView
    {
        [super loadView];
    	self.view.backgroundColor = [UIColor redColor];
    	
    	self.toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 44.0)];
    	self.toolbar.autoresizesSubviews = YES;
    	self.toolbar.barStyle = UIBarStyleBlackOpaque;
    	self.toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
    }
    
    - (void)viewDidLoad;
    {
        [super viewDidLoad];
        OUIWithoutAnimating(^{
            // Don't steal the toolbar items from any possibly open document
            if (!self.forPreviewGeneration) {
                self.toolbar.items = [[OUIDocumentAppController controller] toolbarItemsForDocument:self.document];
                [self.toolbar layoutIfNeeded];
            }
        });
    }
    
    - (UIToolbar *)toolbarForMainViewController;
    {
        if (!self.toolbar)
    	{
    		[self view];
    	}
        return self.toolbar;
    }
    
    - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
    {
        [self _updateTitleBarButtonItemSizeUsingInterfaceOrientation:toInterfaceOrientation];
        
        [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
    }
    
    - (void)willMoveToParentViewController:(UIViewController *)parent;
    {
        if (parent) {
            [self _updateTitleBarButtonItemSizeUsingInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
        }
        
        [super willMoveToParentViewController:parent];
    }
    
    - (void)_updateTitleBarButtonItemSizeUsingInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
    {
        AppDelegate *controller = [AppDelegate controller];
        UIBarButtonItem *titleItem = [controller documentTitleToolbarItem];
        UIView *customView = titleItem.customView;
        
        CGFloat newWidth = UIInterfaceOrientationIsPortrait(interfaceOrientation) ? 400 : 550;
    	
        customView.frame = (CGRect){
            .origin.x = customView.frame.origin.x,
            .origin.y = customView.frame.origin.y,
            .size.width = newWidth,
            .size.height = customView.frame.size.height
        };
    }
    

    These were also taken mostly from Omni’s sample code. All they’re doing is creating a toolbar when the view is loaded, and putting it into place. Whenever something like a rotation event happens, the toolbar is repositioned.

    That’s all for now. You should be able to create new documents in the picker, view those documents (which just show up as a red screen), and return from viewing a document to the picker.

    There’s a bug in here somewhere which means the document title isn’t centred in the toolbar. I’m not sure what’s happening there, so if anyone knows why, please let me know!

    image

  8. Integrating the Omni frameworks into an app

    24th June 2013

    The OmniGroup, makers of apps such as OmniGraffle, have released a lot of their code as the open source Omni Frameworks. This is great: there are things like a document picker with support for OmniPresence, a rich text editor, classes for zoomable tiled views, and many many other things too.

    Unfortunately, the Omni Frameworks are barely documented. There is a sample app, called TextEditor, but despite the existence of this app it took me quite a while to work out how to get the frameworks to compile and to present a blank document picker on screen in a sample app. Here I’ll try to document some of what I did.

    A quick disclaimer: I’m still learning about the Omni Frameworks. I don’t know if I’m doing things right or not, nor can I answer any of your questions. This isn’t meant to be a tutorial that tells you every step, so if it doesn’t work for you, you may have to do some investigation on your own.

    One thing to note is that on iOS I prefer not to use Interface Builder, but rather to set everything up through code. Omni’s sample app uses IB, so that’s one difference I had to account for.

    Setting things up

    This was my first real foray into using Xcode 4’s Workspaces. I set things up this way because that’s how Omni’s sample project was set up. So start by making a workspace for your app, with your app’s project inside it.

    I first cloned the git repository into a directory within my project directory. Then I added some of the Omni Frameworks projects as subprojects to my project:

    image

    As shown in that screenshot, I added OmniBase, OmniFoundation, OmniQuartz, OmniAppKit, OmniFileStore, OmniFileExchange, OmniUI, OmniUIDocument and OmniUnzip as subprojects of my project. I also added FixStringsFile as another project within the same workspace.

    I also dragged all the Configurations files (the .xcconfig files, from in the Configurations directory within the OmniFrameworks root) into a new group in my app’s Xcode project.

    I added the Omni libraries as target dependencies for my app’s target:

    image

    I added a new Run Shell Script build phase, to run Omni’s CopyLibraryResources script:

    image

    (note that the path to the script is the path from your Workspace file. I’ve put the OmniFrameworks in a folder called OmniGroup, which is why that is the first path component.)

    I made sure to link my app with the Omni frameworks, and any frameworks they required:

    image

    I told Xcode to use the appropriate configuration files for the different targets and build types. This is also the first time I used configuration files.

    image

    Constructing the app

    Now to put the app together. As I said, I dislike using nib files on iOS, so the first thing I did was to remove the project’s main interface filename:

    image

    Now to do enough to get a document picker on screen. (Note that we’re not going to make it do anything yet, we’re just going to make it appear.)

    Make your app delegate class inherit from OUIDocumentAppController:

    #import <UIKit/UIKit.h>
    #import <OmniUIDocument/OUIDocumentAppController.h>
    
    @interface AppDelegate : OUIDocumentAppController
    
    @property (strong, nonatomic) UIWindow *window;
    
    @end
    
    

    Then, in AppDelegate.m, let’s get a window displaying with a document picker in it:

    #import "AppDelegate.h"
    #import <OmniUIDocument/OUIDocument.h>
    #import <OmniUIDocument/OUIDocumentPicker.h>
    #import <OmniUIDocument/OUIDocumentPickerDelegate.h>
    #import <OmniUIDocument/OUIMainViewController.h>
    
    @interface AppDelegate () 
    
    @end
    
    @implementation AppDelegate<OUIDocumentPickerDelegate>
    
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        
    	self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    	[self.window makeKeyAndVisible];
    	
    	self.documentPicker = [[OUIDocumentPicker alloc] init];
    	self.documentPicker.delegate = self;
    	
    	self.mainViewController = [[OUIMainViewController alloc] init];
    	
    	self.window.rootViewController = self.mainViewController;
    	
    	return [super application:application didFinishLaunchingWithOptions:launchOptions];
    }
    @end

    Then build and run. You should get something like this:

    image

    That’s all for now. I haven’t explored how to use any of the Omni classes yet. I may post more once I do.

  9. Goals revisited

    18th June 2013

    To anyone who just reads my blog for the tech content, here’s another one you’ll want to skip over. This is an update on my blog post from three months ago, where I listed four medium term goals.

    1: Learn to drive

    I’ve been having lessons. They’re terribly expensive, but I’m getting there. I’m currently working on some of the manoeuvres, and next lesson promises to introduce the parallel park.

    2: Finish my PhD thesis

    I’m currently taking a week and a half off from work in order to work on it. There’s a lot of stuff that’s complete, but a lot still to do!

    3: Get my house sorted ready for new housemates in September

    I’ve found and commissioned a builder to partition the rooms. I’m currently working frantically to sort out various issues, as two new housemates are in fact moving in in July. It doesn’t help that everything keeps going wrong: the shower has just broken, which is another £400 to fix! Still, I am progressing with things.

    4: Make sure I regularly meet with friends

    I’ve been doing a fair bit of this. The problem is mainly that with all the other things on my to-do list, finding the time is harder! Still, I don’t feel I’m doing too badly here.

    So, to conclude: this blog entry was written for my own benefit, rather than that of my readers. It’s important to remind myself that, when every task that gets completed just serves to throw another five new ones my way, I am actually making progress. If anyone else is feeling that they never accomplish anything, or that they’re overwhelmed by their to-do list, try writing a goals list like this one and revisit it after three months. You might be surprised!

  10. What’s still wrong with Apple Maps?

    16th April 2013

    I’ve been thinking about Apple Maps. It’s the only map software I use on my iPhone — I haven’t installed Google’s app — but I don’t like it as much as Google Maps on iOS 5. I tried to unpack why that is, and came up with three main points. Fix these, and it’ll be useable.

    1. The points of interest database is very lacking

    This is the main one. I was in Harrogate the other day, and I searched for “Harrogate Turkish Baths”. It was nowhere to be found. Searching for “Harrogate Baths” found a load of useless results, such as “Lakeland” (a household goods shop). 

    Generic searches aren’t that great either. Searching for “Cake shop” in Harrogate had me travel to Leeds. It also found “Pie Shop” in Rippon — that’s a localisation fail IMO, as “Pie Shop” implies savoury pies in England. 

    Also, it often finds business over places. If I search for “High Street”, I will not be looking for “High Street Pharmacy”.

    2. The search uses the current position of the map view as scope, but not the current user location

    When you search in Apple maps, it cleverly takes into account where you are looking. If the map is centred on Manchester and I search for “Pizza Hut”, it’ll find me some Pizza Hut restaurants in Manchester. This is fine when you’re searching for a brand that’s found in many places, or if you’re searching for something generic like “Newsagent”.

    It’s also fine if you’re searching for something where there is only one in the country. If you put the town in the text you search for (like “Yuyi Dragon, Coventry”) then this works.

    The problem is if you’re searching for something semi-generic but not that common, such as a road name. Maps tries to search the area on the screen, but then can’t find any places. What it actually does then is keeps zooming out and trying the search again, until it’s found somewhere. This is confusing, as it has moved the screen often quite a distance, but it’s unrelated to where the user is. So if, as is typical, I’d left the map centred on wherever I had been last, and then I searched for a road name (intending to search for such a road near where I physically am), it’d find the closest thing matching that road name near where the map was showing.

    I’m not sure what the perfect answer to this is, but Google didn’t annoy me in this manner. It might be related to Apple’s search tending to favour business names over place names — I’m more often searching for a road, town or village than I am a business. (It should be noted that searching for fully disambiguated street addresses works fine — it’s the fuzzy matching that is poor.)

    3. Motorways should be blue. A Roads should be green.

    I can’t get past this. In the UK, roads on maps have standard colours. Making them all white or orange is not OK. I’ve tried to get used to it, but it’s just not possible.

    It’s often vital to know the classification of a road. As a pedestrian, you can’t walk down motorways or some A-roads. Lots of people don’t like driving on motorways, especially for short journeys.

    Apple have correctly coloured the road number labels (e.g. “M4” or “A238”), but the roads themselves remain uncoloured.