iOS Setup
Inbox
The Inbox (sometimes referred to as Notification Center) is a Rover feature for presenting a user's previously received notifications to them.
There are two means for using the Inbox in your app: using Rover's provided UI (either an Activity for presenting modally or a custom view for embedding within your own UI) with your own customizations, or consuming the Notification Store API for building your own bespoke UI.
Presenting the Inbox
The RoverNotifications module contains a view controller with everything needed to fetch and display the user's notifications in a familiar list view. Additionally it supports functionality for marking notifications as read and allowing the user to delete notifications when they are no longer needed.
The only step required to add the Inbox to your app is to resolve the view controller and present it in response to some user interaction. The most common implementations are to present the Inbox modally in response to a button tap or as one of the tabs in a tab bar.
Presenting Modally
The following example demonstrates how to present the Inbox in response to a button tap.
class MyViewController: UIViewController {
// An IBAction connected to a UIButton through Interface Builder
@IBAction func presentNotificationCenter(_ sender: Any) {
// Resolve the Inbox view controller
guard let inbox = Rover.shared.resolve(UIViewController.self, name: "inbox") else {
return
}
present(inbox, animated: true, completion: nil)
}
}
The Inbox understands when it's being presented modally and automatically includes a "Done" button in the top left corner so you do not have to worry about dismissing the Inbox manually.
Presenting in a Tab Bar
Rover's resolve mechanism doesn't support Interface Builder so to add the Inbox to a tab bar it must be done programmatically. The below example demonstrates how this can be done from within the viewDidLoad()
method of a UITabBarController
subclass.
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// Resolve the Inbox view controller
guard let inbox = Rover.shared.resolve(UIViewController.self, name: "inbox") else {
return
}
// Set the Inbox's `tabBarItem` which defines how its tab is displayed
inbox.tabBarItem = UITabBarItem(title: "Inbox", image: nil, tag: 0)
// Add the Inbox to the tab bar's list of view controller's
self.viewControllers?.append(inbox)
}
}
Customizing Rover's Inbox UI - UIKit
The Inbox uses a standard UITableView
internally. You can override the Rover Inbox to customize the UITableViewCell
that is returned in order to customize the appearance of your Inbox.
Firstly, have a look in InboxCell.swift to see what your opportunities for overrides are.
Define your own Inbox Cell class like the following with the changes necessary to suit your product spec:
public class MyInboxCell : InboxCell {
// ... override methods as you deem fit here.
}
Then you need to override our Inbox View Controller class in order to specify the use of your custom Cell:
public class MyCustomInboxViewController : InboxViewController {
override public func registerReusableViews() {
// Rover by default has a UITableViewCell called InboxCell. You can replace it here with your own implementation:
tableView.register(MyInboxCell.self, forCellReuseIdentifier: "inboxCell")
}
}
Then define a custom Rover Assembler to wire up your customized Inbox to the rest of Rover:
public struct CustomInboxAssembler : Assembler {
public func assemble(container: Container) {
container.register(UIViewController.self, name: "inbox") { resolver in
let presentWebsiteActionProvider: MyCustomInboxViewController.ActionProvider = { [weak resolver] url in
resolver?.resolve(Action.self, name: "presentWebsite", arguments: url)
}
return MyCustomInboxViewController(
dispatcher: resolver.resolve(Dispatcher.self)!,
eventQueue: resolver.resolve(EventQueue.self)!,
imageStore: resolver.resolve(ImageStore.self)!,
notificationStore: resolver.resolve(NotificationStore.self)!,
router: resolver.resolve(Router.self)!,
sessionController: resolver.resolve(SessionController.self)!,
syncCoordinator: resolver.resolve(SyncCoordinator.self)!,
presentWebsiteActionProvider: presentWebsiteActionProvider
)
}
}
}
Then include it in your call to Rover.initialize
:
Rover.initialize(assemblers: [
// ... (the other assemblers go here, as usual).
CustomInboxAssembler() // make sure you put this last in the list!
])
Using your own UI
If you want to instead use your own UI (perhaps with notifications from other sources aggregated into the same view), you can instead use the SDK's NotificationStore
to retrieve the notifications and perform the necessary actions to open and delete notifications.
SwiftUI
The following examples are built using SwiftUI. If you are using UIKit, you can still use the NotificationStore
to retrieve notifications.
Retrieving Notifications
A view model can be used to allow for live updates of the Inbox. The following example shows how the NotificationStore
can be used to get and filter the list of notifications, then make them available to the view as a published property. This is achieved with the NotificationStore.notifications
method to get the list of notifications, and the NotificationStore.addObserver
method to observe changes to the list of notifications.
import Foundation
import RoverFoundation
import RoverNotifications
class InboxSampleViewModel: ObservableObject {
private var notificationStore: NotificationStore
private var notificationObserver: NSObjectProtocol?
@Published var notifications: [RoverNotifications.Notification] = []
init() {
notificationStore = Rover.shared.resolve(NotificationStore.self)!
notifications = filterNotifications()
// Observe the notification store for changes and update the notifications list when a change occurs.
notificationObserver = notificationStore.addObserver { [weak self] _ in
DispatchQueue.main.async {
if let self = self {
self.notifications = self.filterNotifications()
}
}
}
}
/**
Returns a filtered list of notifications from dataStore.notifications. This implementation filters out notifications that aren't inbox enabled, are deleted or have expired.
You can change this method if you wish to modify the rules used to filter notifications. For example: if you wish to include expired notifications in the table view and instead show their expired status with a visual indicator.
*/
func filterNotifications() -> [RoverNotifications.Notification] {
return notificationStore.notifications.filter { notification in
guard notification.isNotificationCenterEnabled, !notification.isDeleted else {
return false
}
if let expiresAt = notification.expiresAt {
return expiresAt > Date()
}
return true
}
}
}
Making use of this view model, the following example shows how to build a simple list view that displays the titles, body content and images of the notifications. It also shows how to mark notifications as read and delete notifications.
import SwiftUI
import RoverFoundation
import RoverNotifications
struct InboxSampleView: View {
@ObservedObject var viewModel: InboxSampleViewModel
var body: some View {
NavigationView {
List (viewModel.notifications, id:\.id) { notification in
InboxListItem(notification: notification)
}
.navigationTitle("Inbox Sample")
}
}
}
struct InboxListItem: View {
var notification: RoverNotifications.Notification
// The URL of the notification's image attachment if it exists.
var imageUrl: URL? {
guard let attachment = notification.attachment,
attachment.format == .image else {
return nil
}
return attachment.url
}
var body: some View {
HStack {
if imageUrl != nil {
AsyncImage(url: imageUrl,
content: { image in
image.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 44, maxHeight: 44)
},
placeholder: {
EmptyView()
})
}
VStack(alignment: .leading) {
Text(notification.body)
Text(notification.title ?? "")
.font(.caption)
}
Spacer()
Image(systemName: notification.isRead ? "envelope.open" : "envelope")
.frame(alignment: .trailing)
}
.onTapGesture {
openNotification()
}
.swipeActions {
Button(role: .destructive) {
withAnimation(.linear(duration: 0.3)) {
deleteNotification()
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
Opening Notifications
Continuing from the previous example code, when a notification is opened, the following steps will need to be performed:
- Marking the notification as read
- Handling the notification's tap behavior
- Tracking conversions
- Adding the notification opened event to the event queue
The following sample code demonstrates how to perform these steps for your custom UI.
extension InboxListItem {
func openNotification() {
// 1. Mark the notification as read
if !notification.isRead {
Rover.shared.resolve(NotificationStore.self)!
.markNotificationRead(notification.id)
}
// 2. Handle the notification's tap behavior
switch notification.tapBehavior {
case .openApp:
break
case .openURL(let url):
//Rover uses UIApplication.open(url:,options:,completionHandler:) here, but this can be changed
break
case .presentWebsite(let url):
//This is where Rover would present the website within your app, but this can be changed.
break
}
// 3. Perform conversion tracking
Rover.shared.resolve(ConversionsTrackerService.self)!
.track(notification.conversionTags)
// Setup the Rover event
let attributes: Attributes = [
"notification": notification.attributes,
"source": NotificationSource.notificationCenter.rawValue
]
let eventInfo = EventInfo(
name: "Notification Opened",
namespace: "rover",
attributes: attributes
)
// 4. Add the event to the event queue
Rover.shared.resolve(EventQueue.self)!
.addEvent(eventInfo)
}
}
Marking Notifications as Deleted
Notifications can also be marked as deleted. This is useful if you want to allow users to delete notifications from the Inbox. The following example demonstrates how to mark a notification as deleted.
extension InboxListItem {
func deleteNotification() {
Rover.shared.resolve(NotificationStore.self)!
.markNotificationDeleted(notification.id)
}
}
Delivering Notifications into the Inbox
When authoring a campaign in the Rover Campaigns app, you have the option to enable "Inbox" as one of the ways to deliver the campaign.
When Inbox is enabled, the notification delivered by the campaign will be accessible in your app's Inbox in addition to the system-displayed push notification.