iOS Message Center

The Message Center is a user-addressable, rich message listing. It requires little to no integration to get started and supports some basic theming options.

This guide covers the various tools available for creating custom Message Center implementations, starting with the simplest and moving on to more advanced use cases.

Installation

Message Center requires adding the AirshipMessageCenter module:

For CocoaPods, import using:

import AirshipKit
@import AirshipKit;

For everything else use:

import AirshipMessageCenter
@import AirshipMessageCenter;

Listener

listener for messages update

MessageCenter.shared.inbox.messagePublisher
    .receive(on: RunLoop.main)
    .sink(receiveValue: { messages in
        self.messages = messages
    })
    .store(in: &self.subscriptions)

listener for unread message count

MessageCenter.shared.inbox.unreadCountPublisher
    .receive(on: RunLoop.main)
    .sink { unreadCount in
        self.unreadCount = unreadCount
    }
    .store(in: &self.subscriptions)

Message Center Filtering

Sometimes it can be useful to filter the contents of the Message Center according to some predetermined pattern. To facilitate this, use the shared MessageCenter instance to set a predicate. Once set, only messages that match the predicate will be displayed.

Filtering messages

class CustomPredicate: MessageCenterPredicate {
    func evaluate(message: MessageCenterMessage) -> Bool {
        return message.title.contains("cool")
    }
}

MessageCenter.shared.predicate = CustomPredicate()

If you are embedding the MessageCenterView directly, you can pass the predicate in through the view extension .messageCenterPredicate(_:).

Filtering messages

class CustomPredicate: MessageCenterPredicate {
    func evaluate(message: MessageCenterMessage) -> Bool {
        return message.title.contains("cool")
    }
}

MessageCenterView(
    controller: MessageCenterController()
)
.messageCenterPredicate(CustomPredicate())
            

Styling the Message Center

Using code:

The Message Center’s look can be customized by creating a MessageCenterTheme instance, setting its style properties, and then setting the style property of the Message Center instance to the customized style instance.

Styling the Message Center

var theme = MessageCenterTheme()
theme.cellTitleFont = .title
theme.cellDateFont = .body
theme.cellTitleColor = .primary
theme.cellDateColor = .secondary
theme.unreadIndicatorColor = .blue

// Set the theme on the default Message Center
MessageCenter.shared.theme = theme
// Not supported. Use plist file instead

Using plist file:

This can also be done without writing code. Create a plist with the desired Message Center style. All the keys correspond to properties on the MessageCenterTheme class.

Colors are represented by strings, either a named color or a valid color hexadecimal:

  • Named color: Must correspond to a named color defined in a color asset within the main bundle.
  • Hexadecimal color: We have differents keys to specify light/dark mode colors (i.e., cellTitleColor and cellTitleColorDark).

Save the plist as MessageCenterTheme.plist and include it in the application’s target

Styling the Message Center using a custom style

Create a custom style object that implements MessageCenterViewStyle.

struct CustomMessageCenterViewStyle: MessageCenterViewStyle {
    @ViewBuilder
    func makeBody(configuration: Configuration) -> some View {
        if #available(iOS 16.0, *) {
            NavigationStack {
                configuration.content
                    .navigationBarTitleDisplayMode(.inline)
                    .navigationTitle(Text("Custom Message Center"))
                    .toolbarBackground(.mint, for: .navigationBar)
                    .toolbarBackground(.visible, for: .navigationBar)
                    .toolbarColorScheme(configuration.colorScheme)
            }
        } else {
            NavigationView {
                configuration.content
                    .navigationTitle(Text("Old Custom Message Center"))
                    .navigationBarTitleDisplayMode(.large)
            }
            .navigationViewStyle(.stack)
        }
    }
}

Set the custom style on your Message Center view.

MessageCenterView()
    .messageCenterViewStyle(
        CustomMessageCenterViewStyle()
    )
Styling the Message Center using a plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>refreshTintColor</key>
    <string>#333333</string>
    <key>refreshTintColorDark</key>
    <string>#DDDDDD</string>
    <key>iconsEnabled</key>
    <true/>
    <key>placeholderIcon</key>
    <string>placeholderIcon</string>
    <key>cellTitleFont</key>
    <dict>
        <key>fontName</key>
        <string>ChalkboardSE-Regular</string>
        <key>fontSize</key>
        <integer>16</integer>
    </dict>
    <key>cellDateFont</key>
    <dict>
        <key>fontName</key>
        <string>ChalkboardSE-Regular</string>
        <key>fontSize</key>
        <integer>14</integer>
    </dict>
    <key>cellColor</key>
    <string>#DDDDDD</string>
    <key>cellColorDark</key>
    <string>#333333</string>
    <key>cellTitleColor</key>
    <string>#000000</string>
    <key>cellTitleColorDark</key>
    <string>#FFFFFF</string>
    <key>cellDateColor</key>
    <string>#222222</string>
    <key>cellDateColorDark</key>
    <string>#CCCCCC</string>
    <key>cellSeparatorStyle</key>
    <string>none</string>
    <key>cellSeparatorColor</key>
    <string>#FFFFFF</string>
    <key>cellSeparatorColorDark</key>
    <string>#000000</string>
    <key>cellTintColor</key>
    <string>#FF0000</string>
    <key>cellTintColorDark</key>
    <string>#00FF00</string>
    <key>unreadIndicatorColor</key>
    <string>#FF0000</string>
    <key>unreadIndicatorColorDark</key>
    <string>#FF0000</string>
    <key>selectAllButtonTitleColor</key>
    <string>#333333</string>
    <key>selectAllButtonTitleColorDark</key>
    <string>#DDDDDD</string>
    <key>deleteButtonTitleColor</key>
    <string>#333333</string>
    <key>deleteButtonTitleColorDark</key>
    <string>#DDDDDD</string>
    <key>markAsReadButtonTitleColor</key>
    <string>#333333</string>
    <key>markAsReadButtonTitleColorDark</key>
    <string>#DDDDDD</string>
    <key>hideDeleteButton</key>
    <true/>
    <key>editButtonTitleColor</key>
    <string>#333333</string>
    <key>editButtonTitleColorDark</key>
    <string>#DDDDDD</string>
    <key>cancelButtonTitleColor</key>
    <string>#333333</string>
    <key>cancelButtonTitleColorDark</key>
    <string>#DDDDDD</string>
    <key>backButtonColor</key>
    <string>#333333</string>
    <key>backButtonColorDark</key>
    <string>#DDDDDD</string>
    <key>navigationBarTitle</key>
    <string>Nav Bar Title</string>
</dict>
</plist>
Customizing the Message Center navigation stack

To customize the Message Center’s parent navigation stack in SwiftUI, set its built-in navigation stack to type .none, wrap it in a NavigationStack, and directly apply your preferred configuration, such as display modes, styles, or toolbar items. Here’s an example of how to achieve this:

NavigationStack {
    MessageCenterView()
        .messageCenterNavigationStack(.none) // Disables default navigation stack
        .navigationTitle("Custom Message Center Title")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("Settings") {
                    // Custom action for the toolbar button
                }
            }
        }
}

Localization

The 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.";

Custom Inbox Implementations

While the 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 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 Message Center as such, we can still use the UI classes it depends on. These can all be styled using the same UAMessageCenterTheme class covered in the section above, allowing for more rapid development.

Library Components

Custom Message Center implementations will depend on at least some combination of the classes and protocols described below. For additional detail, please see the Airship Library Reference.

UAMessageCenter
UAMessageCenter is the main entry point for fetching and accessing messages, as well as signing up for delegate callbacks.
UAMessageCenterInbox
UAMessageCenterInbox provides an interface for retrieving messages asynchronously, as well as a reference to the local message array itself.
 Note

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.

UAMessageCenterMessage
UAMessageCenterMessage 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 UAMessageCenterUser credentials. Normally the default UI will handle most details involving message display transparently, but deep customizations ultimately involve displaying these URLs in a webview.
UAMessageCenterDisplayDelegate
UAMessageCenterDisplayDelegate 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.
NativeBridge
Most Message Center messages are rich HTML pages displayed in a web view. If you need to create a custom implementation with your own webview, UANativeBridge (WKWebView) can be used to simplify integration with Airship features such as the Actions Framework.

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

Create and Register a custom UAMessageCenterDisplayDelegate

Implementing the UAMessageCenterDisplayDelegate 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, it also has the effect of overriding the Message Center implementation.

When the Message Center module receives a Message Center action, the first thing it does is check whether a Message Center delegate has been set. If not, the SDK will forward the action to the default Message Center implementation.

Create the Delegate

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 CustomMessageCenterDelegate : NSObject, MessageCenterDisplayDelegate {

    func displayMessageCenter(forMessageID messageID: String) {
       // Display the custom message center for a given message ID
    }

    func displayMessageCenter() {
        // Display the custom messgae center
    }

    func dismissMessageCenter() {
        // Dismiss the custom message center
    }

}
@import AirshipMessageCenter;

@interface CustomMessageCenterDelegate : NSObject <UAMessageCenterDisplayDelegate>

@end
#import "CustomMessageCenterDelegate.h"

@implementation CustomMessageCenterDelegate

- (void)displayMessageCenter {
    // Display the custom message center for a given message ID
}

- (void)displayMessageCenterForMessageID:(NSString * _Nonnull)messageID {
    // Display the custom messgae center
}

- (void)dismissMessageCenter {
    // Dismiss the custom message center
}

@end

Register the Delegate

Next, in order for the UAMessageCenterDisplayDelegate 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
    let messageCenterDelegate = CustomMessageCenterDelegate()
    MessageCenter.shared.displayDelegate = messageCenterDelegate

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

    // Set a custom delegate for handling Message Center events
    CustomMessageCenterDelegate *messageCenterDelegate = [[CustomMessageCenterDelegate alloc] init];
    UAMessageCenter.shared.displayDelegate = messageCenterDelegate;

    // ...
}

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

SwiftUI Message Center

You can use the default swiftUI Message Center and customize the theme, the Message Center item view and the Message Center message view.

MessageCenterView(
    controller: messageCenterController
)
.messageCenterTheme(theme)
.setMessageCenterItemViewStyle(messageCenterListItemViewStyle)
.setMessageCenterMessageViewStyle(messageViewStyle)

Badge Updates

The value of the badge can be set in a number of ways. Most often, the number corresponds with some notion of unread or unviewed content, e.g., email messages or friend requests. The most common way to update the badge is directly through APNs, either by passing along the badge integer in the payload of a push notification, or by telling the app to adjust the value using our autobadge feature.

For applications implementing the Message Center, you may want the badge value to represent the actual state of unread messages in the Message Center. If so, then you need to set this behavior in the application delegate.

Updating badge value to Message Center unread count

func sceneWillResignActive(_ scene: UIScene) {
    Task{
        UIApplication.shared.applicationIconBadgeNumber = await MessageCenter.shared.inbox.unreadCount
    }
}
- (void)sceneWillResignActive:(UIScene *)scene {
    [[[UAMessageCenter shared] inbox] getUnreadCountWithCompletionHandler:^(NSInteger unreadCount) {
        dispatch_async(dispatch_get_main_queue(), ^{
            UIApplication.sharedApplication.applicationIconBadgeNumber = unreadCount;
        });
    }];
}

If you use this method then you should never include badge values in the payload of push notifications, because Message Center will override them.

App Transport Security

Message Center content features such as Landing Pages are only able to load SSL-encrypted web content out of the box. This iOS feature, known as App Transport Security, requires all web connections to use SSL, and only allows for exceptions on an individual basis. Content hosted on Airship’s servers is already SSL-encrypted, but if you plan to use your own content, you will need to make sure it is accessible behind SSL so that iOS will not prevent the SDK from loading it. While we generally recommend leaving these default settings alone, if your app will need to load plain HTTP content then you can add individual domains in your app’s Info.plist file. For more information, see Apple’s App Transport Security Technote.