Adding Coach Marks to an iOS App with DDCoachMarks

For years I’ve wanted to have an in-app tutorial in my PrayerMate app that would use what are known in the UX business as “coachmarks” – a kind of guided tour of the basic functionality of the app whenever it is first installed. Prompted by a colleague, I finally decided to give it a go recently, and settled on using Darin Doria’s DDCoachMarks library for iOS (I also had to implement this on Android using a different library). Here’s the results:

I had to make a few little tweaks to the library so that it would handle long captions in its bubbles and so on. That left two main challenges: handling rotations, and handling scrolling.

Handling rotations

If a user rotates their device half way through a tutorial, DDCoachmarks kind of freaks out. This is understandable, since we’ve defined all of our bubbles in screen co-ordinates, and those co-ordinates completely change when a device rotates. I solved it by making my DDCoachMarksViewDelegate monitor for device orientation change events. Just before my [coachMarksView start] call I added this:


// Start listening for rotation events
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];

[coachMarksView start];

Then I handle those events like so:

- (void)deviceOrientationDidChange:(NSNotification *)notification {

//Obtaining the current device orientation
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];

//Ignoring specific orientations
if (orientation == UIDeviceOrientationFaceUp || orientation == UIDeviceOrientationFaceDown || orientation == UIDeviceOrientationUnknown) {
return;
}

// We need to allow a slight pause before running handler to make sure rotation has been processed by the view hierarchy
[self performSelectorOnMainThread:@selector(handleDeviceOrientationChange:) withObject:self.currentCoachMarksView waitUntilDone:NO];
}

- (void)handleDeviceOrientationChange:(DDCoachMarksView*)coachMarksView {

// Begin the whole coach marks process again from the beginning, rebuilding the coachmarks with updated co-ordinates
coachMarksView.coachMarks = [self coachMarksAfterRotation:coachMarksView];
[coachMarksView start];
}

Of course you also then need to stop listening for rotation events after the coachmarks are finished:

- (void)coachMarksViewWillCleanup:(DDCoachMarksView *)coachMarksView {
// Stop listening for orientation changes
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
}

Handling scrolling

The bigger headache was that some of my coach marks were related to UI elements that were off the bottom of the screen, requiring me to scroll to bring them into view.

To handle this, I have a delegate callback each time a new coachmark is shown:


- (void)coachMarksView:(DDCoachMarksView*)coachMarksView willNavigateToIndex:(NSUInteger)index {
// This is app-specific code to look up the UIScrollView that's currently being looked at
UIScrollView* scrollView = [self.rootViewController currentScrollView];
if (scrollView == nil) {
// No scrolling possible
return;
}

// Look up the details of the coach mark that's about to be displayed
NSDictionary* coachMark = [coachMarksView.coachMarks objectAtIndex:index];

// I added some extra info to my coachmarks dictionary to indicate which coachmarks might require scrolling
if ([coachMark objectForKey:@"scrollView"] == nil) {
// No scrolling required - so always scroll back up to the top
[scrollView setContentOffset:CGPointZero animated:YES];
return;
}

CGRect markRect = [[coachMark objectForKey:@"rect"] CGRectValue];

// Modify coachmarks according to scroll offset
CGRect modifiedRect = CGRectMake(markRect.origin.x, markRect.origin.y - scrollView.contentOffset.y, markRect.size.width, markRect.size.height);

// See if the coachmark is offscreen or not
if (!CGRectContainsRect([self visibleRectForCoachMarks:coachMarksView], modifiedRect)) {
if (scrollView != nil) {
// Scroll until it's in view //
///////////////////////////////
// Convert from screen co-ordinates into the co-ordinate system of the UIScrollView
CGRect markInScrollView = [self.rootViewController.view convertRect:markRect fromView:self.rootViewController.navigationController.view];
markInScrollView = [scrollView convertRect:markInScrollView fromView:self.rootViewController.view];

// Record the current scroll position
CGPoint originalOffset = scrollView.contentOffset;

// Then scroll without animation to work out how far we need to scroll
[scrollView scrollRectToVisible:markInScrollView animated:NO];

// Modify coachmarks rectangle by scrollView offset
markRect.origin.y -= scrollView.contentOffset.y;
markRect.origin.x -= scrollView.contentOffset.x;

// Update the coachmarks array
NSMutableArray *newCoachmarks = [coachMarksView.coachMarks mutableCopy];
NSMutableDictionary* mutable = [coachMark mutableCopy];
[mutable setObject:[NSValue valueWithCGRect:markRect] forKey:@"rect"];

[newCoachmarks setObject:mutable atIndexedSubscript:index];
coachMarksView.coachMarks = newCoachmarks;

// Now put the UIScrollView back where it started and ANIMATE it into position - this is just so that it looks nicer
scrollView.contentOffset = originalOffset;
[scrollView scrollRectToVisible:markInScrollView animated:YES];
}
}
}

Conclusion

And there we have it! Beautiful coachmarks and a user who knows exactly how your app works as though you were right there next to them explaining it all.

Many thanks to Darin Doria for his hard work on this handy little iOS library.

Announcing PrayerMate for Android v3

Thanks to last autumn’s crowdfunding campaign, I had the privilege of being able to hire a developer to work full time on PrayerMate for Android for a couple of months. Dave’s main focus was on implementing automatic syncing (more news on that in a couple of weeks) but along the way he was also able to add a whole load of features from the iOS app that were missing from Android. I’ve also done a bunch of work myself, and outsourced yet more to some developers in Eastern Europe. The result is PrayerMate for Android v3, which I’m delighted to announce is now live on Google Play. Here’s just a few of the new features it includes:

1. Address Book Integration

One of the hardest parts of getting going with PrayerMate has always been getting your initial list of friends set up. There’s now a handy way of adding friends directly from your address book. When you hit the “+” button, you’ll get a new option “Create from address book”:

android_v3_quick_add

This will then pull up a list of your contacts. Select the ones you want to create subjects from, and it will then create a new entry for each one.

Related to this is the ability to send messages to people as you pray for them. When praying for somebody, press and hold on the card for the context menu, and hit the “Send message” option. You then get to choose whether to email or to send a text message:

android_v3_send_message

If you created somebody using the “Create from address book” option then their email / mobile number will be prefilled. I hope soon to be able to add the option to retrospectively link an existing subject to an address book entry, but I haven’t had the chance yet because of all the other features I’ve been working on!

2. New category options

If you’ve ever used PrayerMate to pray for your small group Bible study, you’ve probably wished there was an easy way to email people’s prayer requests out to your group. Now you can – when viewing a category, tap the “…” button in the top right and you’ll see two new options: “Pray through this category” and “Email these subjects”:

android_v3_category_options

3. Selectively export categories

When exporting to Dropbox, you can now selectively export just certain categories. This gives you an easier way to share prayer points with others – as long as they’re in their own category, you can export them to Dropbox which can then be easily imported by others. Just hit the “Export to Dropbox” option under Advanced Settings and you can choose which categories you want to export (the default is still to export them all):

android_v3_export_cats

4. Region-specific feed galleries

When you open up the feed gallery, users in the UK, United States, Canada and Australia should now see a customised list of featured feeds that is more geographically relevant:

android_v3_feed_gallery

5. Recently prayed list

Tap the cog button and you’ll now see a “Recently prayed” option to be able to find subjects that you’ve prayed for recently:

android_v3_recently_prayed

6. New subject order options

When you tap into a category’s settings menu, hit “Change subject order” and as well as being able to manually reorder subjects, you’ll now also get options to sort them alphabetically, randomise the order, or turn on the new “auto-shuffle feature”. Sometimes people find that always praying for people in exactly the same order gets a bit repetitive. Turn on “auto-shuffle” on a particular category, and every time you’ve prayed through all the subjects in that category it will randomise the order for the next time you pray.

android_v3_sort_subjects

Other changes

Other recent changes you may have missed is the concept of “private feeds” – churches or organisations who sign up at http://www.prayermate.net/publishers can now publish feeds that don’t have to be publicly advertised in the feed gallery. You’ll be given both a private URL and a QR code that you can share with your supporters, and they can use either one to subscribe.

You might also notice slightly more dynamic content appearing in prayer feeds, as they now support links and embedded images.

PrayerMate is a free app on iOS and Android to help you pray regularly and faithfully. As well as all of your personal prayer points for friends and family, you can also subscribe to regular updates from over 100 different Christian charities and churches, as well as downloading suggested prayers from the gallery.