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
Sample notifications
A notification with an attachment:
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).
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.
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.
Related
In my ComplicationController, I'm creating my descriptor with an NSUserActivity object:
class ComplicationController: NSObject, CLKComplicationDataSource {
// MARK: - Complication Configuration
let data = MyData.shared
var dataDict: Dictionary<AnyHashable, Any> = ["character": "nil"]
func getComplicationDescriptors(handler: #escaping ([CLKComplicationDescriptor]) -> Void) {
let userActivity = NSUserActivity(activityType: "com.myapp")
userActivity.userInfo = dataDict
let descriptors = [
CLKComplicationDescriptor(
identifier: "myapp.extralarge",
displayName: "MyApp",
supportedFamilies: [CLKComplicationFamily.extraLarge],
userActivity: userActivity)
]
// Call the handler with the currently supported complication descriptors
handler(descriptors)
}
In my ExtensionDelegate, I can detect that the complication has been tapped and use Notification Center to send a message to my Watch App as follows:
func handle(_ userActivity: NSUserActivity) {
if let date = userActivity.userInfo?[CLKLaunchedTimelineEntryDateKey] as? Date {
// Handoff from complication
NotificationCenter.default.post(
name: Notification.Name.complicationTapped,
object: date
)
}
I can detect the timeline entry date using the CLKLaunchedTimelineEntryDateKey. How do I detect what data was being presented by the complication? I can't find where to set a key on the NSUserActivity object.
I'd like to unit test some UNUserNotificationCenterDelegate methods, especially userNotificationCenter(_, willPresent:, withCompletionHandler:)
To do that, I'd have to create an instance of UNNotification, but that's not really possible because it only has an initWithCoder initializer. What to do?
This is an example of what I'd like to test:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler(.sound)
}
Thanks to this README and the private iOS headers here, I came up with this solution:
func testUserNotificationCenterDelegate() throws {
// Create the notification content
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Test"
notificationContent.userInfo = ["someKey": "someValue"]
// Create a notification request with the content
let notificationRequest = UNNotificationRequest(identifier: "test", content: notificationContent, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false))
// Use private method to create a UNNotification from the request
let selector = NSSelectorFromString("notificationWithRequest:date:")
let unmanaged = UNNotification.perform(selector, with: notificationRequest, with: Date())
let notification = unmanaged?.takeUnretainedValue() as! UNNotification
// Test the method
let callbackExpectation = XCTestExpectation(description: "Callback")
pushService.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) { (options) in
XCTAssertEqual(options, .sound)
callbackExpectation.fulfill()
}
wait(for: [callbackExpectation], timeout: 2.0)
}
In the code above, pushService is an instance of a class that conforms to the UNUserNotificationCenterDelegate protocol. Elsewhere in my code, I set that instance as the center's delegate: UNUserNotificationCenter.current().delegate = pushService.
Enjoy!
You can use class_createInstance(UNNotification.classForKeyedArchiver() ...)and cast the value as UNNotification
If you want to manipulate its content and date members you can subclass UNNotification and use this same formula «changing class name and cast» to create it, then you override those members- which are open- and return whatever you want
I have tried to add a banner notification generator to my macOS swift app and the banner does not appear when test running in XCode and neither are there any new notifications visible in the notification centre. Other apps on my computer are generating notifications regularly. What have I missed? I have granted permission when requested
My app delegate is as follows
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
#IBOutlet weak var mainMenu: NSMenu!
func applicationDidFinishLaunching(_ aNotification: Notification)
{
NSUserNotificationCenter.default.delegate = self ;
}
func userNotificationCenter(_ center: NSUserNotificationCenter, shouldPresent notification: NSUserNotification) -> Bool
{
return true
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
On app startup I run the following method and I see the console line "Notifications allowed"
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])
{ granted, error in
if error != nil
{
print ("Request notifications permission Error");
};
if granted
{
self.allowNotifications = true ;
print ("Notifications allowed");
}
else
{
self.allowNotifications = false ;
print ("Notifications denied");
};
}
The method I have added to my ViewController is as follows and I have tested that the print statement at the end is reached
func generateNotification (summary:String, sound:String, title:String , body:String)
{
let notification = NSUserNotification()
if !allowNotifications {return};
notification.title = summary ;
notification.subtitle = title ;
notification.informativeText = body ;
if (sound == "YES") {notification.soundName = NSUserNotificationDefaultSoundName};
NSUserNotificationCenter.default.deliver (notification);
print ("notification generated");
};
Please help me
I believe that my problem here was asking permission to use UNUserNotification and then using NSUserNotification to create the notification itself, which of course I had not requested permission to use. Requesting permission is now mandatory in Catalina (and perhaps it was in earlier versions of macOS as well.)
So I replaced the generateNotification function with the following and it all works correctly.
let notificationCenter = UNUserNotificationCenter.current();
notificationCenter.getNotificationSettings
{ (settings) in
if settings.authorizationStatus == .authorized
{
//print ("Notifications Still Allowed");
// build the banner
let content = UNMutableNotificationContent();
content.title = summary ;
content.body = title ;
if sound == "YES" {content.sound = UNNotificationSound.default};
// could add .badge
// could add .userInfo
// define when banner will appear - this is set to 1 second - note you cannot set this to zero
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false);
// Create the request
let uuidString = UUID().uuidString ;
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger);
// Schedule the request with the system.
notificationCenter.add(request, withCompletionHandler:
{ (error) in
if error != nil
{
// Something went wrong
}
})
//print ("Notification Generated");
}
In addition to the answer of Steve Brooker, for me it was working only when I set the delegate for UNUserNotificationCenter. I've spent half a day trying to make it work with NSUserNotificationCenter / NSUserNotification without any success. So thanks for your answer, Steve :)
My working version is as follows:
if #available(OSX 10.14, *) {
UNUserNotificationCenter.current().delegate = self // must have delegate, otherwise notification won't appear
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) {
[weak self] granted, error in
print("Permission granted: \(granted)")
guard granted else { return }
let sound = "NO"
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.getNotificationSettings
{ (settings) in
if settings.authorizationStatus == .authorized {
//print ("Notifications Still Allowed");
// build the banner
let content = UNMutableNotificationContent();
content.title = "summary" ;
content.body = "title" ;
if sound == "YES" {content.sound = UNNotificationSound.default};
// could add .badge
// could add .userInfo
// define when banner will appear - this is set to 1 second - note you cannot set this to zero
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false);
// Create the request
let uuidString = UUID().uuidString ;
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger);
// Schedule the request with the system.
notificationCenter.add(request, withCompletionHandler:
{ (error) in
if error != nil
{
// Something went wrong
}
})
//print ("Notification Generated");
}
}
}
} else {
// Fallback on earlier versions
}
I need to mock UNNotificationResponse and UNNotification so that I can test my implementation of:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Swift.Void)
However I can't usefully subclass these classes because init() is specifically marked as unavailable, resulting in compilation errors like this if I try:
/Path/to/PushClientTests.swift:38:5: Cannot override 'init' which has been marked unavailable
What alternate approaches can be taken here? I look into going down the Protocol Oriented Programming route, however since I do not control the API being called, I can't modify it to take the protocols I'd write.
To do it you do the following.
Get a real example of the object while debugging and save in file system using your simulator.
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {
let encodedObject = NSKeyedArchiver.archivedData(withRootObject: notification)
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/notification.mock"
fileManager.createFile(atPath: path, contents: encodedObject, attributes: nil)
Find the object in your Mac and add the file in the same target as the test class.
Now unarchive in your test.
let path = Bundle(for: type(of: self)).path(forResource: "notification", ofType: "mock")
let data = FileManager.default.contents(atPath: path ?? "")
let notification = NSKeyedUnarchiver.unarchiveObject(with: data ?? Data()) as? UNNotification
I've used the next extension to create UNNotificationResponse and UNNotification instances while implementing unit tests for push notifications on iOS:
extension UNNotificationResponse {
static func testNotificationResponse(with payloadFilename: String) -> UNNotificationResponse {
let parameters = parametersFromFile(payloadFilename) // 1
let request = notificationRequest(with: parameters) // 2
return UNNotificationResponse(coder: TestNotificationCoder(with: request))! // 3
}
}
Loads push notification payload from file
Creates UNNotificationRequest instance with specified parameters in userInfo
Creates UNNotificationResponse instance using NSCoder subclass
Here are the functions I've used above:
extension UNNotificationResponse {
private static func notificationRequest(with parameters: [AnyHashable: Any]) -> UNNotificationRequest {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Test Title"
notificationContent.body = "Test Body"
notificationContent.userInfo = parameters
let dateInfo = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date())
let trigger = UNCalendarNotificationTrigger(dateMatching: dateInfo, repeats: false)
let notificationRequest = UNNotificationRequest(identifier: "testIdentifier", content: notificationContent, trigger: trigger)
return notificationRequest
}
}
fileprivate class TestNotificationCoder: NSCoder {
private enum FieldKey: String {
case date, request, sourceIdentifier, intentIdentifiers, notification, actionIdentifier, originIdentifier, targetConnectionEndpoint, targetSceneIdentifier
}
private let testIdentifier = "testIdentifier"
private let request: UNNotificationRequest
override var allowsKeyedCoding: Bool { true }
init(with request: UNNotificationRequest) {
self.request = request
}
override func decodeObject(forKey key: String) -> Any? {
let fieldKey = FieldKey(rawValue: key)
switch fieldKey {
case .date:
return Date()
case .request:
return request
case .sourceIdentifier, .actionIdentifier, .originIdentifier:
return testIdentifier
case .notification:
return UNNotification(coder: self)
default:
return nil
}
}
}
Short answer: You can't!
Instead, decompose your implementation of
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Swift.Void)
and test the methods you call from there, instead.
Happy testing :)
It appears you can initialize UNNotificationContent objects. I've chosen to rework my push handling methods to take UNNotificationContent objects instead of UNNotificationResponse/UNNotification.
If the UNNotificationResponse value doesn't matter, and you just want to execute that method of your app delegate, you can accomplish this by creating a mock by subclassing NSKeyedArchiver like this:
class MockCoder: NSKeyedArchiver {
override func decodeObject(forKey key: String) -> Any { "" }
}
You can then call it like this:
let notificationMock = try XCTUnwrap(UNNotificationResponse(coder: MockCoder()))
appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: notificationMock) { }
Your app delegate's userNotificationCenter(_:didReceive:withCompletionHandler:) method will now have been called, allowing you to assert to your heart's content (assuming no assertions against the notification itself, at least).
You can use class_createInstance(UNNotification.classForKeyedArchiver() ...)and cast the value as UNNotification
If you want to manipulate its content and date members you can subclass UNNotification and use this same formula «changing class name and cast to your subclass» to create it, then you override those members- which are open- and return whatever you want
What code do I write to trigger a watch kit notification from the watch app itself? For example, if I connect a button from my watch storyboard to my WatchInterfaceController as an action then when pressed it triggers a notification on the watch.
For trigger a notification, first of all you need permission: (declared usually in the ExtensionDelegate)
func askPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert,.sound]) { (authBool, error) in
if authBool {
let okAction = UNNotificationAction(identifier: "ok", title: "Ok", options: [])
let category = UNNotificationCategory(identifier: "exampleCategoryIdentifier", actions: [okAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
UNUserNotificationCenter.current().delegate = self
}
}
}
For having this working, you need to import (in the ExtensionDelegate) "UserNotifications" and to extend:
UNUserNotificationCenterDelegate
Once you have done that, you can call askPermission where you want, in a way like this:
if let delegate = WKExtension.shared().delegate as? ExtensionDelegate {
delegate.askPermission()
}
Now you have (hopefully) the permission for trigger a notification!
For trigger a notification, you can use a function like this:
func notification() {
let content = UNMutableNotificationContent()
content.body = "Body Of The Notification"
content.categoryIdentifier = "exampleCategoryIdentifier" // Re-Use the same identifier of the previous category.
content.sound = UNNotificationSound.default() // This is optional
let request = UNNotificationRequest(identifier: NSUUID().uuidString,
content: content,
trigger: nil)
let center = UNUserNotificationCenter.current()
center.add(request) { (error) in
if error != nil {
print(error!)
} else {
print("notification: ok")
}
}
}
In order to test watch notifications, you must first create a new build scheme.
Duplicate your watch app scheme, and in the "Run" section, choose your custom notification as the executable.
Now you can run the notification scheme.
Inside the extensions group in your project, under Supporting Files is a file called PushNotificationPayload.json.
You can edit the payload file to try different notifications and categories.