How to show 2 Buttons with UNNotification macOS - swift

I am delivering a UNNotification via UNUserNotificationCenter.
//AppDelegate
let notificationCenter = UNUserNotificationCenter.current()
// applicationDidFinishLaunching
// Add Action button to notification
let mountButton = UNNotificationAction(identifier: "test1", title: "Accept", options: .destructive)
let actionButton2 = UNNotificationAction(identifier: "test2", title: "Cancel", options: .destructive)
let notificationCategory = UNNotificationCategory(identifier: "content_added_notification", actions: [mountButton, actionButton2], intentIdentifiers: [])
notificationCenter.setNotificationCategories([notificationCategory])
func sendNotification(title: String, body: String = "") {
let content = UNMutableNotificationContent()
content.title = title
if body.isEmpty {
content.body = body
}
content.subtitle = "subtitle title... "
content.categoryIdentifier = "content_added_notification"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let uuidString = UUID().uuidString
let request = UNNotificationRequest(
identifier: uuidString,
content: content,
trigger: trigger)
notificationCenter.add(request) { (error: Error?) in
if let theError = error {
print(theError)
}
}
}
This works just fine, except that Actions are not shown:
The Action Buttons only show up when hoovering over the notification, and to make it worse, both Buttons are grouped under an Options Button, that needs to be clicked:
According to the Apple Docs, the system should be able to show 2 Buttons:
https://developer.apple.com/documentation/usernotifications/unnotificationcategory
Categories can have associated actions, which define custom buttons the system displays for notifications of that category. When the system has unlimited space, the system displays up to 10 actions. When the system has limited space, the system displays at most two actions.
This does not match with pictures like the one below, so
How can i make the notification appear like this:.

Since macOS 11, the design of the notifications has been changed for unknown reasons. You must hover to make the actions visible, there is no way to get the far better old pre-macOS 11 layout.

Related

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
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.

Is it possible to mutate the content of a local notification in swift?

In the next version of my app, I want to integrate the possibility that the user can enable weekly reports like the screentime feature from apple.
I know how to setup Local Notifications (The code is already working for other types of notifications like reminders for events), but for this idea, I need the notification text to be updated shortly before firing because I don`t know the exact content of the notification when I add it to the NotificationCenter.
Is there a function which is called shortly before firing or how would I do this?
This is my working code for scheduling notifications:
fileprivate func generateNotification(title: String, body: String, triggerDate: DateComponents) -> String{
//Trigger
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: false)
//Content
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = UNNotificationSound.default
//Request
let identifier = UUID().uuidString
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in
if let error = error {
print("Error \(error.localizedDescription)")
}
}
return identifier
}

Change Repeating Reminder's Content Daily

In my app, I ask the user to set up a repeating reminder. In this reminder, I would like the body to say something different each day. For example, I have over 500 quotes in my Firebase db and I want my reminder to show a new quote each day. How can I programmatically change the body of the reminder each day without user interaction?
#IBAction func saveButtonPressed(_ sender: Any) {
let content = UNMutableNotificationContent()
let identifier = "myApp"
content.title = "myApp"
content.body = "I want to change this programatically each day"
let trigger = UNCalendarNotificationTrigger(dateMatching: Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: reminderTime.date), repeats: true)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request){
(error) in
if error != nil
{
print("here error in setting up notification")
print(error!)
}
else
{
print("notification scheduled")
}
}
}
Unfortunately, you can not handle the LocalNotification showing moment. You just tell the system when to show your notification and the system show it without calling your application.
But you can add multiple notification requests at once (up to 64). For example, all notifications for the next month and update your notifications on app launch. Just make sure that your notification requests has different identifiers.
For example, you create 30 notifications and user receives them for 5 days. Then he opens your app and you add 5 notifications more.
If you'll need to remove some of the pending notifications, you can make it using the following method.
let center = UNUserNotificationCenter.current()
center.removePendingNotificationRequests(withIdentifiers: identifiers)

swift, detect ibeacons on the background and send notifications when in range

Hello I'm new at IBeacons and beginner on swift and I'm trying to make a small app that detects Ibeacon on the background of the app and send a notification when the Ibeacon is in range I manage to do so but only when I walk while the app is open I could not make it work and search for Ibeacons on the background even though I gave the app access to take the location on the background by using
if (CLLocationManager.authorizationStatus() != CLAuthorizationStatus.authorizedAlways) {
locationManager.requestAlwaysAuthorization();
}
this is my main problem, I have a side problem too that the app does not save the person name if the app is closed and opened again the app will forget the name. Here is my code I'd really appreciate your help so much also please if you have any references to learn more about IBeacons applications I'd appreciate it
import UIKit
import CoreLocation
import UserNotifications
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var field: UITextField!
#IBOutlet weak var textLbl : UILabel!
var inRoom = false
var name = ""
var sendYet = false ;
func sendHiNoti()
{
name = field.text! ;
let content = UNMutableNotificationContent()
content.title = "Heloo "+name
content.subtitle = "Welcome to your Home"
content.body = "this messaage to welcome you in Home"
content.badge = 1
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "quiteimpressed.mp3"))
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false)
let request = UNNotificationRequest(identifier: "azanSoon", content: content, trigger: trigger)
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
UNUserNotificationCenter.current().add(request) {(error) in
if let error = error {
print("error: \(error)")
}
}
}
func sendByeNoti()
{
name = field.text! ;
let content = UNMutableNotificationContent()
content.title = "OH"+name
content.subtitle = "You are going out already ??"
content.body = "Take care of your self"
content.badge = 1
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "quiteimpressed.mp3"))
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false)
let request = UNNotificationRequest(identifier: "azanSoon", content: content, trigger: trigger)
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
UNUserNotificationCenter.current().add(request) {(error) in
if let error = error {
print("error: \(error)")
}
}
}
#IBAction func getText(){
name = field.text!
let alert = UIAlertController(title: "Your Name is", message: name, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge], completionHandler: {didAllow, error in})
let uuid = UUID(uuidString: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")!
let beaconRegion = CLBeaconRegion(proximityUUID: uuid, major: 444, minor: 333, identifier: "abcdefac005b")
if (CLLocationManager.authorizationStatus() != CLAuthorizationStatus.authorizedAlways) {
locationManager.requestAlwaysAuthorization();
}
locationManager.startRangingBeacons(in: beaconRegion)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
print(beacons)
if(beacons.count > 0){
if(!sendYet){
if beacons[0].proximity.rawValue < 2 {
textLbl.text = "In the room"
sendHiNoti()
sendYet = true ;
}
}
else if beacons[0].proximity.rawValue >= 3 {
textLbl.text = "Outside the room"
sendByeNoti()
sendYet = false ;
}
}
}
}
The code shown uses beacon ranging locationManager.startRangingBeacons(in: beaconRegion) which is generally not supported in the background for more than 10 seconds after transition between foreground and background.
The locationManager.requestAlwaysAuthorization() will only unlock the ability to use beacon monitoring in the background. Beacon monitoring gives you a single call when beacons either first appear (didEnter(region: region)) or all disappear(didExit(region: region)).
This is the only beacon API which works in the background under normal circumstances.
It is possible to do beacon ranging in the background for longer than 10 seconds using two techniques:
You can get 180 seconds of background ranging after the app transitions to the background by starting a background task as described in my blog post here.
You can also tell iOS that you are a location app to unlock unlimited background beacon ranging. You must first implement the solution in part 1. Then, in your Info.plist, declare:
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
Finally, in your code run locationManager.startUpdatingLocation(). This will cause your app to receive regular updates of its latitude/longitude, but as a side effect allows you background task from step 1 to run forever, allowing ranging to continue forever in the background.
If you choose to use option 2, beware that it will be more difficult to get your app approved for sale in the AppStore. You must convince Apple reviewers that your app is a legitimate location app (like Waze or Apple Maps) and the user is aware that your app is always running in the background. If you do not convince them of this, they will reject your app.
Separately, it is simple to save off values to persistent storage so they are retained across app restarts. Just use NSUserDefaults like this:
// save off name when user fills it in
UserDefaults.standard.set(name, forKey: "name")
// load back name on app restart
name = UserDefaults.standard.string(forKey: "name")

How to trigger a WK Notification

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.