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:

  1. Marking the notification as read
  2. Handling the notification's tap behavior
  3. Tracking conversions
  4. 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.

Alert Options

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.

Inbox Preview

Previous
Push Notifications