Wednesday, December 22, 2010

CMPopTipView - a custom popup view for iOS


CMPopTipView
is a custom iOS UIView that I have released on github.  It is relatively simple, but solved a need I had in an app where I wanted a kind of speech bubble shaped popup to give the user a hint about where to tap next. (You might argue that good UI shouldn't need an extra hint for the user but then I would punch you in the arm and go about my day.)




CMPopTipView is simple to use.  Just initialise it with a target view, a container view and a message to display and it handles positioning itself with the container view to point at the target view, sizing itself to fit the text.  It is also smart about UIBarButtonItems (in either navigation or tool bars) and can position itself properly to point at them. Some example code is shown below.


Some features of CMPopTipView:

  • Automatically positioned in container view;
  • Positions pointer to point at target view (either above or below);
  • Automatically sizes itself to fit the text message;
  • Background and text colours can be customised;
  • User can tap CMPopTipView to dismiss it (with optional delegate callback);
  • Can be dismissed programmatically;
  • Not a modal view.

CMPopTipView draws itself using Core Graphics and so scales up nicely to the high resolution Retina display with no extra work.  It might be a useful reference for iOS developers looking for Core Graphics drawing examples.  The code demonstrates how to draw the path of a custom shape, how to draw a gradient fill within a clipped path, how to apply a shadow to the shape and how to draw text within the shape.


A universal (iPhone/iPad) demo app is included with the source code. It is a button-fest of popup tips, as shown by the screenshots.

Example 1 - point at a UIBarButtonItem in a nav bar:

// Present a CMPopTipView pointing at a UIBarButtonItem in the nav bar
CMPopTipView *navBarLeftButtonPopTipView = [[[CMPopTipView alloc] initWithMessage:@"A Message"] autorelease];
navBarLeftButtonPopTipView.delegate = self;
[navBarLeftButtonPopTipView presentPointingAtBarButtonItem:self.navigationItem.leftBarButtonItem animated:YES];

// Dismiss a CMPopTipView
[navBarLeftButtonPopTipView dismissAnimated:YES];

// CMPopTipViewDelegate method
- (void)popTipViewWasDismissedByUser:(CMPopTipView *)popTipView {
  // Any cleanup code, such as releasing a CMPopTipView instance variable, if necessary
} 




Example 2 - pointing at a UIButton, with custom color scheme:

- (IBAction)buttonAction:(id)sender {
  // Toggle popTipView when a standard UIButton is pressed
  if (nil == self.roundRectButtonPopTipView) {
    self.roundRectButtonPopTipView = [[[CMPopTipView alloc] initWithMessage:@"My message"] autorelease];
    self.roundRectButtonPopTipView.delegate = self;
    self.roundRectButtonPopTipView.backgroundColor = [UIColor lightGrayColor];
    self.roundRectButtonPopTipView.textColor = [UIColor darkTextColor];

    UIButton *button = (UIButton *)sender;
    [self.roundRectButtonPopTipView presentPointingAtView:button inView:self.view animated:YES];
  }
  else {
    // Dismiss
    [self.roundRectButtonPopTipView dismissAnimated:YES];
    self.roundRectButtonPopTipView = nil;
  }
}

#pragma mark - CMPopTipViewDelegate methods
- (void)popTipViewWasDismissedByUser:(CMPopTipView *)popTipView {
  // User can tap CMPopTipView to dismiss it
  self.roundRectButtonPopTipView = nil;
}
Get CMPopTipView from github.

Friday, October 22, 2010

iPad External Screen Mirroring

At last week's Melbourne CocoaHeads meeting I did a presentation and demo of Locayta Search Mobile. What was cool about it (besides the content of course!) was that I did the whole presentation – including live demos – from the iPad, using the external VGA adapter. iPad's Keynote works well with the VGA adapter (besides a few niggles) as you would expect. However, there's no automatic mirroring of standard apps to the external display like there is on the Mac which makes live demoing a little more tricky.

So how did I perform live iPad app demos on the projector? Well, I didn't jailbreak to run background screen mirroring; and I didn't spend ages customising the demo apps to draw their UI on both the iPad and external displays (both of which are possible options).

I fortunately found a little hack on Google Code called iphoneos-screen-mirroring.  It simply provides a UIApplication category that gives you a single method to call to enable mirroring of the iPad display to the external display.  The minimum amount of code you'd need to add to your app delegate is:


#import "UIApplication+ScreenMirroring.h"

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    /* ... your app set up ... */
    // Start screen mirroring to Video Out (if connected)
    [[UIApplication sharedApplication]
      setupScreenMirroringWithFramesPerSecond:5];
}

The screen mirroring hack has no effect until an external adapter is plugged in to the iPad. When an external display is connected it kicks off a loop that continuously snapshots the main iPad display and copies the contents to the external display.  The mirroring frame rate can be customised when setting it up (like in the above example) and the reason to lower the frame rate is because the snapshotting kills the iPad performance quite a bit.  Not enough to prevent demoing of apps that don't refresh the display too much, but animations and scrolling suffer quite a bit.

For my case the hack worked well enough at 5 fps.  If you were trying to demo a 30 fps game I think you'd be a little disappointed.

Note that the mirroring hack uses UIGetScreenImage() to snapshot the main screen and so is not App Store "safe".

A similar project, robterrell's TVOutManager, claims to provide an App Store friendly technique (with certain caveats).  However, I found the performance to be slightly worse with that one and I wasn't planning on submitting the mirroring code to the App Store so I stuck with iphoneos-screen-mirroring.

Tuesday, October 12, 2010

Locayta Notes shows off iOS full text search

Locayta Notes
This week at Locayta we released an iPad/iPhone (universal) app to demonstrate the Locayta Search Mobile full text search library for iOS. Locayta Notes is a free iPad/iPhone note taking app which obviously offers full text search of the contents of all notes. Search is updated in real time as notes are being written or modified. It includes all the fancy search features such as word stemming, automatic spell correction, user defined synonyms and various sorting options.

But that's not all! Locayta has also released the full source code for Locayta Notes under an open source license. The source code should be a very useful resource for Locayta Search Mobile developers. All that is needed to build the app is the Locayta Search Mobile SDK, a free download from Locayta (currently in beta).

Finally, I will be doing a talk and demo of Locayta Search Mobile at this month's Melbourne CocoaHeads meeting on Thursday. Hope to see you there if you can make it.

Thursday, October 7, 2010

iOS 4 Background Task Completion for River Level

In the latest River Level update (v1.0.2) I added background task completion. River Level syncs waterway statistics from the server and even though the syncing is performed in a background thread while the app is running – so the user can still navigate around the app while data is being updated – if the user closes the app any active syncing task will be aborted.

iOS 4 allows tasks to complete in the background even after the user closes the app (e.g. hits the Home button or switches to another app). This is called background task completion and is part of the multitasking feature set. Adding background task completion was surprisingly simple. Now, River Level running on any device with iOS 4+ will complete any data syncing tasks in the background if the user closes the app while syncing is active, which is a better experience for the user.

The key to adding background task completion is to wrap any tasks in calls to the UIApplication methods: beginBackgroundTaskWithExpirationHandler: and endBackgroundTask:.

beginBackgroundTaskWithExpirationHandler: signals iOS that your app would like the task to complete even if the app is moved into the background state, before the app becomes suspended.  endBackgroundTask: signals iOS that your app's task has completed, allowing it to move your app to the suspended state if it needs to.  If the app remains in the foreground the whole time then these calls have no effect, so it doesn't hurt to use them.

Note that background tasks don't get free reign and don't get an indefinite period of time to complete. The "expiration handler" part of beginBackgroundTaskWithExpirationHandler: is a block specifying your cleanup code if the task out lives its lifetime.  Code executing as a background task must not make any UI updates or openGL calls (as the app is offscreen).

In River Level I use code similar to the example below.  If background tasks are supported on the host device (see Determining Whether Multitasking Support is Available) then I call beginBackgroundTaskWithExpirationHandler: to request background task completion before starting the syncing operation. When the syncing completes (and again, if background tasks are supported) and the task has not already been invalidated for some reason, I simply call endBackgroundTask: to tell iOS my task is done.  If the app was running in a background state at this time and no other background tasks were active, iOS would move the app to the suspended state.

if ([RiverLevelAppDelegate sharedAppDelegate].backgroundSupported) {
// Register to execute task in background if required
UIApplication *app = [UIApplication sharedApplication];
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}

// kick off the asynchronous syncing task...

// ... when the syncing task completes:

if ([RiverLevelAppDelegate sharedAppDelegate].backgroundSupported) {
// Flag completion of background task
if (bgTask != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplicationendBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
}


Also note that you are not limited to only one background task, you can register as many as you need although they all must complete before the expiry time.  River Level registers a background task for each waterway when batch updates are requested.

Thursday, August 19, 2010

Looking for Beta testers for new iOS full text search engine library


At Locayta we have released a beta of our new full text search engine library for iPhone/iPad developers. I have been leading this project from the software engineering side and am looking for developers interested in beta testing this framework and offering some feedback.

To download the beta version, go to http://www.locayta.com/pages/new/ios-search-engine

Here is a quick blurb about the search engine library:

Locayta Seach for iOS is a port of Locayta's full text search engine library for the iOS platform.  The core library is pure C (with a bit of C++) and we have wrapped a higher level Objective-C API around it and produced a static library in a Framework bundle so that iPhone & iPad apps can provide fast local full text search.  The search engine provides "enterprise level" full text search using a probabilistic model of document terms, along with clever features to improve search success such as automatic spell correction (based on trigram analysis of terms) and word stemming.