Build a custom Message Center
Build a custom Message Center by composing the SDK’s ready-to-use views, handling display requests, integrating with your navigation, and filtering messages.
By default, Airship displays the Message Center as an overlay on top of your app. For tighter integration with your app’s navigation, build your own Message Center from the ready-to-use views the SDK provides. Each view is a SwiftUI View you can place anywhere in your app, style, and drive with your own navigation.
Ready-to-use views
The SDK provides a view for each part of the Message Center. Use the highest-level view that fits your needs, and drop to the lower-level views when you need more control.
| View | Use it to |
|---|---|
| Inbox view | Show the complete Message Center, either as an all-in-one view with its own navigation or as content you place inside your own navigation. |
| List view | Show only the message list, with or without a navigation bar. |
| Message view | Show a single message, with or without a navigation bar, or render only the message body. |
Every view reads its appearance from the global MessageCenterTheme. For colors, fonts, and icons, see Applying a custom theme.
Handling display requests
A push notification with a Message Center action, or a call to Airship.messageCenter.display(), triggers a display request. To route these requests to your own UI instead of the default overlay, set the display callbacks after takeOff.
Custom display handling
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
try! Airship.takeOff(config, launchOptions: launchOptions)
// Navigate to your custom UI when a message should be displayed
Airship.messageCenter.onDisplay = { messageID in
// messageID is optional - nil means show the full list
// Return true to prevent the default SDK display
return true
}
// Dismiss your custom UI when the Message Center should close
Airship.messageCenter.onDismissDisplay = {
// Dismiss your custom Message Center UI
}
return true
}@import AirshipObjectiveC;
@interface MyMessageCenterDisplayDelegate : NSObject <UAMessageCenterDisplayDelegate>
@end
@implementation MyMessageCenterDisplayDelegate
- (void)displayMessageCenter {
// Navigate to your custom UI to show the full message list
}
- (void)displayMessageCenterForMessageID:(NSString *)messageID {
// Navigate to your custom UI to show the given message
}
- (void)dismissMessageCenter {
// Dismiss your custom Message Center UI
}
@end
@interface AppDelegate ()
@property (nonatomic, strong) MyMessageCenterDisplayDelegate *messageCenterDisplayDelegate;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[UAirship takeOff:config launchOptions:launchOptions error:nil];
// displayDelegate is weak, so keep a strong reference for as long as you need it
self.messageCenterDisplayDelegate = [[MyMessageCenterDisplayDelegate alloc] init];
UAirship.messageCenter.displayDelegate = self.messageCenterDisplayDelegate;
return YES;
}
@endFiltering messages
Set a predicate to control which messages appear. The SDK evaluates the predicate against every message and displays only those that pass. Apply a predicate globally on Airship.messageCenter, or pass one to an individual view.
Implement MessageCenterPredicate and return true for messages to keep:
public protocol MessageCenterPredicate: Sendable {
func evaluate(message: MessageCenterMessage) -> Bool
}Filter by named user
If multiple users share a device, filter the inbox to the current named user. When you create a message, include a custom key named named_user_id set to the user’s ID. See Add custom keys or the extra object in the Message Center object.
Filter by named user
class NamedUserPredicate: MessageCenterPredicate {
// `namedUserID` is async, so cache the latest value from the publisher
// for use in the synchronous `evaluate` method.
private var namedUserID: String?
private var cancellable: AnyCancellable?
init() {
cancellable = Airship.contact.namedUserIDPublisher
.sink { [weak self] namedUserID in
self?.namedUserID = namedUserID
}
}
func evaluate(message: MessageCenterMessage) -> Bool {
guard let namedUserID = self.namedUserID else {
return false
}
if let messageNamedUserID = message.extra["named_user_id"] {
return messageNamedUserID == namedUserID
}
return false
}
}
Airship.messageCenter.predicate = NamedUserPredicate()@import AirshipObjectiveC;
@interface NamedUserPredicate : NSObject <UAMessageCenterPredicate>
@property (nonatomic, copy, nullable) NSString *namedUserID;
@end
@implementation NamedUserPredicate
- (instancetype)init {
self = [super init];
if (self) {
// namedUserID is fetched asynchronously, so cache the latest value
// for use in the synchronous evaluateWithMessage: method.
__weak typeof(self) weakSelf = self;
[UAirship.contact getNamedUserIDWithCompletionHandler:^(NSString *namedUserID, NSError *error) {
weakSelf.namedUserID = namedUserID;
}];
}
return self;
}
- (BOOL)evaluateWithMessage:(UAMessageCenterMessage *)message {
if (!self.namedUserID) {
return NO;
}
NSString *messageNamedUserID = message.extra[@"named_user_id"];
if (messageNamedUserID) {
return [messageNamedUserID isEqualToString:self.namedUserID];
}
return NO;
}
@end
UAirship.messageCenter.predicate = [[NamedUserPredicate alloc] init];To filter a single embedded view instead of the whole app, pass the predicate to the view. See the inbox view and list view pages.
Key components
- MessageCenter
- The main entry point for fetching messages and handling display callbacks. Access it through
Airship.messageCenter. - MessageCenterInboxProtocol
- Retrieves messages asynchronously and exposes the local message array. Access it through
Airship.messageCenter.inbox. - MessageCenterMessage
- A model object representing an individual message. A message’s content is either a web body or a native (SceneA mobile app or web experience of one or more screens displayed with fully native UI components in real time, providing immediate, contextual responses to user behaviors. Scenes can be presented in full-screen, modal, or embedded format using the default swipe/click mode or as a story. Scene content can also be displayed in a Message Center message and contain survey questions.) layout. Don’t load the body directly—render it with
MessageCenterMessageVieworMessageCenterMessageContentView, which resolve content type and authentication for both. - MessageCenterMessageContentView
- Renders only a message’s content—no loading indicator, error/retry UI, or mark-as-read. Available since SDK 20.10.0. See Rendering message content.
- MessageCenterController
- Shared state that binds the list and message views together and drives navigation. Pass one controller to the views that need to stay in sync.
The message list uses CoreData. Message objects are ephemeral references that refresh with the list. Don’t hold onto individual message instances indefinitely.