iOS Message Center Customization

Starting with Urban Airship SDK 7, apps have access to a default Message Center which requires little to no integration to get started. However, most developers will want to customize the look and feel to match their existing style and layout. This topic guide cover the various tools available for creating custom Message Center implementations, starting with the simplest and moving on to more advanced use cases.

The Default Message Center

As its name suggests, the default Message Center is an out-of-the-box Message Center implementation built into the Urban Airship SDK. Unlike the sample UI that shipped with earlier SDK versions, this implementation is intended for production and is ready for use in any app embedding the new SDK.

The Default Message Center and its related classes are compatible with iOS versions 8 and above. Apps explicitly targeting iOS 7 will need to create totally custom UI implementations, which is beyond the scope of this topic guide.

Basics

By default, when the app receives a push notification carrying a Message Center Action, the Message Center will be automatically displayed in a modal view controller.

The default Message Center can also be displayed manually with a simple method call. By wiring this method call to a button in your app, you can quickly produce a user-initiated Message Center with no additional effort.

UAirship.defaultMessageCenter().display()
[[UAirship defaultMessageCenter] display];

Of course, this default implementation displays with default settings for elements such as colors, fonts and title text. In the following section we’ll explore ways to customize its look and feel without having to create custom UI.

Adding a Custom Look and Feel

By setting a handful of top-level properties on the Default Message Center, you can override the default style to quickly arrive at a look and feel matching that of your app. We’ll cover a few of these to show how easy this process can be.

A good place to start is the title property on UADefaultMessageCenter . This is the title text that displays in the navigation bar above the message list. By default the title is a localized string reading “Message Center”, but it can be overridden by setting the property directly.

UAirship.defaultMessageCenter().title = "My Message Center"
[UAirship defaultMessageCenter].title = @"My Message Center";

For more visual configuration, we’ve provided a UADefaultMessageCenterStyle class, which contains fine-grained properties representing style parameters. For instance, we may want to change the color of the navigation bar in order to match our app, and set custom fonts for the title and table view cells. In the example below, we assume the app has access to the “Roboto” font, but any stock or custom font may be used.

let style = UADefaultMessageCenterStyle()

// Customize the style object
style.navigationBarColor = UIColor(red: 0.988, green: 0.694, blue: 0.106, alpha: 1)
style.titleColor = UIColor(red: 0.039, green: 0.341, blue: 0.490, alpha: 1)
style.tintColor = UIColor(red: 0.039, green: 0.341, blue: 0.490, alpha: 1)

style.titleFont = UIFont(name: "Roboto-Regular", size: 17.0)
style.cellTitleFont = UIFont(name: "Roboto-Bold", size: 14.0)
style.cellDateFont = UIFont(name: "Roboto-Light", size: 12.0)
UADefaultMessageCenterStyle *style = [UADefaultMessageCenterStyle style];

// Customize the style object
style.navigationBarColor = [UIColor colorWithRed:0.988 green:0.694 blue:0.106 alpha:1];
style.titleColor = [UIColor colorWithRed:0.039 green:0.341 blue:0.490 alpha:1];
style.tintColor = [UIColor colorWithRed:0.039 green:0.341 blue:0.490 alpha:1];

style.titleFont = [UIFont fontWithName:@"Roboto-Regular" size:17.0];
style.cellTitleFont = [UIFont fontWithName:@"Roboto-Bold" size:13.0];
style.cellDateFont = [UIFont fontWithName:@"Roboto-Light" size:12.0];

If your app has the ability to send list icons, these can also be enabled by setting a single boolean parameter.

style.isIconsEnabled = true
style.iconsEnabled = YES;

Finally, to apply these customizations to the default Message center, simply assign them to the style parameter.

UAirship.defaultMessageCenter().style = style
[UAirship defaultMessageCenter].style = style;

Taken together, these small changes result in a truly custom look and feel.

Localization

The default Message Center uses string resources to localize its UI. Ordinarily the SDK will read a file named UAMessageCenterUI.strings in the AirshipResources bundle. However, you can override this by adding your own versions of the file to the main bundle of your app. The SDK will search here first, falling back on AirshipResources as necessary.

At present, the strings available for localization are as follows.

"UA_Message_Center_Title" = "Message Center";

"UA_Message" = "Message";

"UA_OK" = "OK";
"UA_Retry" = "Retry";
"UA_Cancel" = "Cancel";

"UA_Loading" = "Updating";
"UA_No_Messages" = "No Messages";
"UA_No_Message_Selected" = "No Message Selected";

"UA_Select_All" = "Select All";
"UA_Select_None" = "Select None";

"UA_Delete" = "Delete";
"UA_Mark_as_Read" = "Mark Read";

"UA_Error_Connection" = "Connection Error";
"UA_Error_Loading_Message" = "Unable to load message. Please try again later.";
"UA_Error_Loading_Message_List" = "Unable to load messages. Please try again later.";

A Deeper Look

While the default Message Center is ideal for many use cases, there may be times when it’s more appropriate to embed a Message Center in a specific location within your app. While the default Message Center attempts to strike a balance between configurability and hassle-free implementation, some implementation decisions can only be made on a case-by-case basis, such as placement and navigation.

In this section we’ll cover some key classes that are useful for creating custom implementations, and walk through one that embeds a Message Center in a tab. Though we won’t be using the default Message Center as such, we can still use the UI classes it depends on. These can all be styled using the same UADefaultMessageCenterStyle class covered in the section above, allowing for more rapid development.

High Level Component Overview

Custom Message Center implementations will depend on at least some combination of the classes and protocols described below.

UAInbox

UAInbox is the main entry point for fetching and accessing messages, as well as signing up for delegate callbacks.

UAInboxMessageList

UAInboxMessageList provides an interface for retrieving messages asynchronously, as well as a reference to the local message array itself. For instance, the example below shows how to retrieve new messages remotely:

UAirship.inbox().messageList.retrieveMessageList(successBlock: {
   // handle success
}, withFailureBlock: {
    // handle failure
})
[[UAirship inbox].messageList retrieveMessageListWithSuccessBlock:^{
    // handle success
} withFailureBlock:^{
    // handle failure
}];

If you are using or subclassing the default UI classes, this operation shouldn’t ordinarily be necessary. On the other hand, if you are designing custom UI, you can use the above call to implement refresh behavior. Accessing the local array of messages is straightforward:

let messages = UAirship.inbox().messageList.messages
NSArray *messages = [UAirship inbox].messageList.messages;

The message list uses CoreData under the hood, and as such the message objects contained within are ephemeral references, refreshed along with the list after retrieval. It is not recommended to hold onto individual instances indefinitely.

UAInboxMessage

UAInboxMessage is the model object representing an individual message available for display. Instances of this class do not contain the actual message body. Rather, they point to the URL corresponding to the message body in the backend. These URLs are authenticated using UAUser credentials. Normally the default UI will handle most details involving message display transparently, but deep customizations ultimately involve displaying these URLs in a webview.

UAInboxDelegate

UAInboxDelegate is a protocol that can be used to sign up for delegate callbacks when messages are available for viewing, or when UI should be shown. This can be useful in custom implementations that embed or subclass the default UI, as your app can use these events to navigate to the appropriate location when a new message (or the entire message center) needs to be displayed.

To register for callbacks, simply assign a delegate to UAInbox :

UAirship.inbox().delegate = myDelegate
[UAirship inbox].delegate = myDelegate;

In the following sections we’ll walk through a custom implementation that uses this feature in more detail.

UAWebViewDelegate

Most message center messages are rich HTML pages displayed in a UIWebView. Our default UI classes handle the details of this for you, but in case you need to create a custom implementation with your own webview, UAWebViewDelegate can be used to simplify integration with Urban Airship features such as the Actions Framework.

Embedding the Message Center UI

In previous versions of the Urban Airship SDK all Message Center implementations were effectively custom jobs, and involved either a complete implementation from scratch or heavily modified sample code and resources. These approaches are still available when the situation is warranted, but for common use cases it is much easier to reuse pieces of the default UI.

In this example we’ll be subclassing UADefaultMessageCenterSplitViewController .

import AirshipKit

class MessageCenterViewController : UADefaultMessageCenterSplitViewController {

}
#import <AirshipKit/AirshipKit.h>

@interface MessageCenterViewController : UADefaultMessageCenterSplitViewController
@end

As a starting point, we can override the viewDidLoad method to assign a custom style to the view controller as we did above to the default Message Center.

override func viewDidLoad() {
     super.viewDidLoad()

     let style = UADefaultMessageCenterStyle()

     let robotoLight = UIFont(name: "Roboto-Light", size: 12.0)
     let robotoBold = UIFont(name: "Roboto-Bold", size: 14.0)
     let robotoRegular = UIFont(name: "Roboto-Regular", size: 17.0)

     style.navigationBarColor = UIColor(red: 0.988, green: 0.694, blue: 0.106, alpha: 1)
     style.titleColor = UIColor(red: 0.039, green: 0.341, blue: 0.490, alpha: 1)
     style.tintColor = UIColor(red: 0.039, green: 0.341, blue: 0.490, alpha: 1)

     style.titleFont = robotoRegular
     style.cellTitleFont = robotoBold
     style.cellDateFont = robotoLight

     self.style = style
 }
- (void)viewDidLoad {
    [super viewDidLoad];

    UADefaultMessageCenterStyle *style = [UADefaultMessageCenterStyle style];

    UIFont *robotoLight = [UIFont fontWithName:@"Roboto-Light" size:12.0];
    UIFont *robotoBold = [UIFont fontWithName:@"Roboto-Bold" size:13.0];
    UIFont *robotoRegular = [UIFont fontWithName:@"Roboto-Regular" size:17.0];

    style.navigationBarColor = [UIColor colorWithRed:0.988 green:0.694 blue:0.106 alpha:1];
    style.titleColor = [UIColor colorWithRed:0.039 green:0.341 blue:0.490 alpha:1];
    style.tintColor = [UIColor colorWithRed:0.039 green:0.341 blue:0.490 alpha:1];

    style.titleFont = robotoRegular;
    style.cellTitleFont = robotoBold;
    style.cellDateFont = robotoLight;

    self.style = style;
}

Next we’ll add a method that allows code from the outside to display a specific message upon request. This will help with the creation of the UAInboxDelegate in the next section. It also happens to be rather easy when using the default UI classes, as demonstrated below.

func displayMessage(message: UAInboxMessage) {
    self.listViewController.displayMessage(message)
}
- (void)displayMessage:(UAInboxMessage *)message {
    [self.listViewController displayMessage:message];
}

Creating and Registering a Custom Inbox Delegate

Implementing the UAInboxDelegate protocol gives your app the chance to respond to events regarding Message Center display, or the display of individual messages within your Message Center. As this is an optional protocol, this also has the effect of overriding the default Message Center Implementation.

When the Urban Airship SDK receives a Message Center action, the first thing it does is check whether an inbox delegate has been set. If not, the SDK will use the default Message Center. Otherwise it will delegate that responsibility to your app.

First, let’s create the delegate class. The structure of the class and design patterns used within are a matter of preference, but for an app built around a tab bar controller, one straightforward approach is to initialize it with the root view controller and use that at runtime to navigate to the appropriate tab when necessary.

import AirshipKit

class InboxDelegate : NSObject, UAInboxDelegate {

    var rootViewController : UIViewController

    init(rootViewController:UIViewController) {
        self.rootViewController = rootViewController
    }

    func messageViewController() -> MessageCenterViewController {
        let tabBarController = self.rootViewController as! UITabBarController
        let viewController = tabBarController.viewControllers![2]
        return viewController as! MessageCenterViewController
    }

    func showInbox() {
        let tabBarController = self.rootViewController as! UITabBarController
        tabBarController.selectedIndex = 2
    }

    func showInboxMessage(message: UAInboxMessage) {
        self.showInbox()
        self.messageViewController().displayMessage(message)
    }
}
#import "InboxDelegate.h"
#import "MessageCenterViewController.h"

@interface InboxDelegate ()
@property(nonatomic, strong) UIViewController *rootViewController;
@end

@implementation InboxDelegate

- (instancetype)initWithRootViewController:(UIViewController *)rootViewController {
    self = [super init];
    if (self) {
        self.rootViewController = rootViewController;
    }
    return self;
}

- (MessageCenterViewController *)messageCenterViewController {
    UITabBarController *tabBarController = (UITabBarController *)self.rootViewController;
    return [tabBarController.viewControllers objectAtIndex:2];
}

- (void)showInboxMessage:(UAInboxMessage *)message {
    [self showInbox];
    [[self messageCenterViewController] displayMessage:message];
}

- (void)showInbox {
    UITabBarController *tabBarController = (UITabBarController *)self.rootViewController;
    tabBarController.selectedIndex = 2;
}

@end

Keep in mind that the index of 2 used above assumes that the MessageCenterController occupies the third tab in the application. Regardless of the implementation details, the fundamental approach remains the same, which is that the delegate is responsible for handling showInbox and showInboxMessage events, and navigating to the tab containing the MessageCenter when needed.

Next, in order for the InboxDelegate to actually receive those events, it must be registered. An easy place to do this would be in the AppDelegate, after takeOff.

func applicationDidFinishLaunching(_ application: UIApplication) {

    // ...

    // Set a custom delegate for handling message center events
    self.inboxDelegate = InboxDelegate(rootViewController: (window?.rootViewController)!)
    UAirship.inbox().delegate = self.inboxDelegate

    // ...
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // ...

    // Set a custom delegate for handling message center events
    self.inboxDelegate = [[InboxDelegate alloc] initWithRootViewController:self.window.rootViewController];
    [UAirship inbox].delegate = self.inboxDelegate;

    // ...
}

With these pieces in place, your embedded Message Center should be ready for use.