Jim
Jim

Reputation: 31

Trying to customize notifications in macOS with Swift

I am using macOS 10.5.6 and I am trying to display a custom notification. I am using UNNotificationAction to set up a drop down menu for the notification and UNNotificationCategory to save it. I can get the notification correctly. The title and body are displayed but the popup menu for the notification is displayed under a button labeled "Actions".

What I would like to happen is have the label "Actions" changed to a two button format the way that the Reminders app does. I have spent a couple of days searching this web site and several others trying to find the answer but all I have found is the method I am currently using to set up the notification with out the button format that I would like to display. I know that it can be done I just do not know which key words to use to get the answer I would appreciate any help I can get.

enter image description here

Upvotes: 3

Views: 1757

Answers (1)

zrzka
zrzka

Reputation: 21219

Sample notifications

A notification with an attachment:

enter image description here

A notification with an attachment, mouse is hovering over to make the action buttons visible (they're visible right away if there's no attachment).

enter image description here

Sample project

Delegate

AppDelegate is going to handle notifications in the following sample project. We have to make it conform to the UNUserNotificationCenterDelegate protocol.

import UserNotifications

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
    ...
}

We have to set the UNUserNotificationCenter.delegate to our AppDelegate in order to receive notifications. It must be done in the applicationDidFinishLaunching: method.

func applicationDidFinishLaunching(_ aNotification: Notification) {
    setupNotificationCategories() // See below
    UNUserNotificationCenter.current().delegate = self
    // Other stuff
}

Authorization, capabilities, ... omitted for simplicity.

Constants

An example how to avoid hardcoded constant.

enum Note {
    enum Action: String {
        case acceptInvitation = "ACCEPT_INVITATION"
        case declineInvitation = "DECLINE_INVITATION"

        var title: String {
            switch self {
            case .acceptInvitation:
                return "Accept"
            case .declineInvitation:
                return "Decline"
            }
        }
    }

    enum Category: String, CaseIterable {
        case meetingInvitation = "MEETING_INVITATION"

        var availableActions: [Action] {
            switch self {
            case .meetingInvitation:
                return [.acceptInvitation, .declineInvitation]
            }
        }
    }

    enum UserInfo: String {
        case meetingId = "MEETING_ID"
        case userId = "USER_ID"
    }
}

Setup categories

Make the notification center aware of our custom categories and actions. Call this function in the applicationDidFinishLaunching:.

func setupNotificationCategories() {
    let categories: [UNNotificationCategory] = Note.Category.allCases
        .map {
            let actions = $0.availableActions
                .map { UNNotificationAction(identifier: $0.rawValue, title: $0.title, options: [.foreground]) }

            return UNNotificationCategory(identifier: $0.rawValue,
                                          actions: actions,
                                          intentIdentifiers: [],
                                          hiddenPreviewsBodyPlaceholder: "",
                                          options: .customDismissAction)
    }

    UNUserNotificationCenter.current().setNotificationCategories(Set(categories))
}

Create a notification content

Sample notification content with an attachment. If we fail to create an attachment we will continue without it.

func sampleNotificationContent() -> UNNotificationContent {
    let content = UNMutableNotificationContent()
    content.title = "Hey Jim! Weekly Staff Meeting"
    content.body = "Every Tuesday at 2pm"
    content.userInfo = [
        Note.UserInfo.meetingId.rawValue: "123",
        Note.UserInfo.userId.rawValue: "456"
    ]
    content.categoryIdentifier = Note.Category.meetingInvitation.rawValue

    // https://developer.apple.com/documentation/usernotifications/unnotificationattachment/1649987-init
    //
    // The URL of the file you want to attach to the notification. The URL must be a file
    // URL and the file must be readable by the current process. This parameter must not be nil.
    //
    // IOW We can't use image from the assets catalog. You have to add an image to your project
    // as a resource outside of assets catalog.

    if let url = Bundle.main.url(forResource: "jim@2x", withExtension: "png"),
        let attachment = try? UNNotificationAttachment(identifier: "", url: url, options: nil) {

        content.attachments = [attachment]
    }

    return content
}

Important: you can't use an image from the assets catalog, because you need an URL pointing to a file readable by the current process.

enter image description here

Trigger helper

Helper to create a trigger which will fire a notification in seconds seconds.

func triggerIn(seconds: Int) -> UNNotificationTrigger {
    let currentSecond = Calendar.current.component(.second, from: Date())

    var dateComponents = DateComponents()
    dateComponents.calendar = Calendar.current
    dateComponents.second = (currentSecond + seconds) % 60

    return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
}

Notification request

let content = sampleNotificationContent()
let trigger = triggerIn(seconds: 5)

let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)

UNUserNotificationCenter.current().add(request) { (error) in
    if error != nil {
        print("Failed to add a notification request: \(String(describing: error))")
    }
}

Handle notifications

Following functions are implemented in the sample project AppDelegate.

Background

This is called when your application is in the background (or even if your application is running, see Foreground below).

func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler:
    @escaping () -> Void) {

    guard let action = Note.Action(rawValue: response.actionIdentifier) else {
        print("Unknown response action: \(response.actionIdentifier)")
        completionHandler()
        return
    }

    let userInfo = response.notification.request.content.userInfo

    guard let meetingId = userInfo[Note.UserInfo.meetingId.rawValue] as? String,
        let userId = userInfo[Note.UserInfo.userId.rawValue] as? String else {
            print("Missing or malformed user info: \(userInfo)")
            completionHandler()
            return
    }

    print("Notification response: \(action) meetingId: \(meetingId) userId: \(userId)")

    completionHandler()
}

Foreground

This is called when the application is in the foreground. You can handle the notification silently or you can just show it (this is what the code below does).

func userNotificationCenter(_ center: UNUserNotificationCenter,
                            willPresent notification: UNNotification,
                            withCompletionHandler completionHandler:
    @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.alert, .badge, .sound])
}

iOS customization

There's another way how to customize the appearance of notifications, but this is not available on the macOS. You have to use attachments.

enter image description here

Upvotes: 4

Related Questions