How to use UNUserNotificationCenter in Swift Command Line Tool? - swift

I want to develop a command line tool for notifications, like https://github.com/julienXX/terminal-notifier and
use new UserNotification API
But now the program is throw a exception:
Optional(Error Domain=UNErrorDomain Code=1 "Notifications are not allowed for this application" UserInfo={NSLocalizedDescription=Notifications are not allowed for this application})
I don't know under what circumstances the Notification Center will throw this exception, and I would like to ask what configuration needs to be added to use the Notification Center in the command line tool.
My code:
import Cocoa
import UserNotifications
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
let content = UNMutableNotificationContent()
content.title = "title"
content.body = "body"
content.userInfo = ["method": "new"]
content.sound = UNNotificationSound.default
content.categoryIdentifier = "NOTIFICATION"
let testCategory = UNNotificationCategory(identifier: "NOTIFICATION",
actions: [],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "",
options: .customDismissAction)
let request = UNNotificationRequest(identifier: "NOTIFICATION_REQUEST",
content: content,
trigger: nil)
// Schedule the request with the system.
let center = UNUserNotificationCenter.current()
center.setNotificationCategories([testCategory])
center.add(request) { (error) in
if error != nil {
// Handle any errors.
print(error)
}
}
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
let app = NSApplication.shared
let appDelegate = AppDelegate()
app.delegate = appDelegate
app.run()

Related

Remove Notications for User while subscribed to app

I am trying to make it so a notification asking the user to subscribe will not appear anymore when the user is subscribed to my app, below i s my notifications class i am implementing, just having a hard time figuring out how to add code in to null out the alert when they are actually subscribed.
import Foundation
import UserNotifications
class Notifications: NSObject, UNUserNotificationCenterDelegate {
func notificationRequest() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("All set!")
} else if let error = error {
print(error.localizedDescription)
}
}
}
func addNotification() {
print("foi")
let content = UNMutableNotificationContent()
content.title = "WARNING: Safari at risk! ⚠️"
content.subtitle = "Tap here to secure your device!"
content.sound = UNNotificationSound.default
// show this notification 3 days from now 259200
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 259200, repeats: true)
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
}
func clearNotification() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
}

How can I open a specific view on notification tap in SwiftUI?

I am trying to send a user to a DetailView once the notification is tapped. I've followed multiple instructions such as this one. However, when I test it out, tapping on the notification still brings me to the wrong view (the settingsView).
In App Delegate, I currently have this setup (for updated code scroll below):
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerBackgroundTasks()
FirebaseApp.configure()
UNUserNotificationCenter.current().delegate = self
return true
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
if response.actionIdentifier == "open" {
NotificationCenter.default.post(name: NSNotification.Name("DetailViewNew"), object: nil)
}
completionHandler()
}
In SettingsDetailView (where the notification is triggered and setup)
func configureNewAlert() {
let center = UNUserNotificationCenter.current()
center.removeAllDeliveredNotifications()
center.removeAllPendingNotificationRequests()
let content = UNMutableNotificationContent()
content.title = "Header"
content.body = "Test"
content.sound = UNNotificationSound.default
if let selectedHour = Calendar.current.dateComponents([.hour], from: userData.wakeUpTime).hour, let selectedMinute = Calendar.current.dateComponents([.minute], from: userData.wakeUpTime).minute{
var dateComponents = DateComponents()
dateComponents.hour = selectedHour
dateComponents.minute = selectedMinute
let open = UNNotificationAction(identifier: "open", title: "Open ", options: .foreground)
let cancel = UNNotificationAction(identifier: "cancel", title: "Close", options: .destructive)
let categories = UNNotificationCategory(identifier: "action", actions: [open,cancel], intentIdentifiers: [])
UNUserNotificationCenter.current().setNotificationCategories([categories])
content.categoryIdentifier = "action"
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let randomIdentifier = UUID().uuidString
let request = UNNotificationRequest(identifier: randomIdentifier, content: content, trigger: trigger)
center.add(request) { error in
if let error = error {
print(error.localizedDescription)
}
}
}
}
And in my DetailView (where I want the user to land when tapping the notification)
struct DetailViewNew: View {
let poemPublisher = NotificationCenter.default.publisher(for: NSNotification.Name("DetailViewNew"))
#State private var showScreen = false
var body: some View {
ZStack{
Text("Text")
}
.onReceive(poemPublisher) { notification in
self.showScreen = true
}
}
}
EDIT
I removed the categories and action, as I just want to have the user either tap the notification or dismiss it (not custom actions required).
When I do so, the completion handler if response.actionIdentifier == UNNotificationDefaultActionIdentifier does indeed get called. Now the question is how do I route it so it opens the DetailView when tapped?
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
if response.actionIdentifier == UNNotificationDismissActionIdentifier {
print ("Message Closed")
}
else if response.actionIdentifier == UNNotificationDefaultActionIdentifier {
print ("App is Open")
NotificationCenter.default.post(name: NSNotification.Name("DetailViewNew"), object: nil)
}
completionHandler()
}
Any suggestions? Thanks.

How to make app start at login or add to login items after install like dropbox? Xcode 11, Swift 5, MacOS 10.13+

I am trying to have my app start automatically when a user logs out and back in or after a reboot. The objective is to have the app there indefinitely unless a user closes out, but, to have it start back up when they reboot.
There are several tutorials I have followed and threads I have read but they all seem outdated. I am using this app on 3 different OS' including 10.13, 10.14 and 10.15. I seem to have most of the issues on 10.15 machines. I can't figure out why it is hit or miss whether the app starts on login and why sometimes it does not.
https://martiancraft.com/blog/2015/01/login-items/
https://theswiftdev.com/how-to-launch-a-macos-app-at-login/
I have code signed the application, sandbox is enabled, but maybe since the tutorials and information I have found is outdated I am missing something outside the code. This app is for internal company use only, and thus, I will not be submitting to the Apple. I intend to deploy to machines using our management software, and have built a .pkg to deploy the application + launcher into the applications folder and to run automatically after install.
Any help, cleanup, explanations or suggestions are welcome and appreciated.
Main App:
extension Notification.Name{
static let killLauncher = Notification.Name("killLauncher")
}
extension AppDelegate: NSApplicationDelegate{
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
//assign variables for launcherhelper
let launcherAppId = "Kinetic.KTGHelperLauncher"
let runningApps = NSWorkspace.shared.runningApplications
let isRunning = !runningApps.filter {$0.bundleIdentifier == launcherAppId }.isEmpty
//set launcher to login item
SMLoginItemSetEnabled(launcherAppId as CFString, true)
//status check if running or not running
if isRunning {
DistributedNotificationCenter.default().post(name: .killLauncher, object: Bundle.main.bundleIdentifier!)
}
//configure button to display button in assets
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("kinetic_websitemain_red"))
}
//builds menu on start
constructMenu()
}
}
#NSApplicationMain
class AppDelegate: NSObject {
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
#objc func TakeScreenshot(_ sender: Any){
//get path to user download folder
let dirPath = FileManager().urls(for:.downloadsDirectory, in:.userDomainMask)[0]
//create time stamp of when picture is taken
func CreateTimeStamp() -> Int32
{
return Int32(Date().timeIntervalSince1970)
}
var displayCount: UInt32 = 0;
var result = CGGetActiveDisplayList(0, nil, &displayCount)
if (result != CGError.success) {
print("error: \(result)")
return
}
let allocated = Int(displayCount)
let activeDisplays = UnsafeMutablePointer<CGDirectDisplayID>.allocate(capacity: allocated)
result = CGGetActiveDisplayList(displayCount, activeDisplays, &displayCount)
if (result != CGError.success) {
print("error: \(result)")
return
}
for i in 1...displayCount {
let unixTimestamp = CreateTimeStamp()
let fileUrl = dirPath.appendingPathComponent("\(unixTimestamp)" + "_" + "\(i)" + ".jpg", isDirectory:false)
let screenShot:CGImage = CGDisplayCreateImage(activeDisplays[Int(i-1)])!
let bitmapRep = NSBitmapImageRep(cgImage: screenShot)
let jpegData = bitmapRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: [:])!
do {
try jpegData.write(to: fileUrl, options: .atomic)
}
catch {print("error: \(error)")}
}
}
#objc func kineticSelf(_ sender: Any){
let kineticSelfUrl = URL(string: "/Library/Addigy/macmanage/MacManage.app")
NSWorkspace.shared.openFile(kineticSelfUrl!.path)
// NSWorkspace.shared.open(URL(fileURLWithPath: "/Library/Addigy/macmanage/MacManage.app"))
}
//function that opens kinetic helpdesk website
#objc func kineticHelpdesk(_ sender: Any){
let kineticHelpdeskUrl = URL(string: "http://helpdesk.kinetictg.com")!
NSWorkspace.shared.open(kineticHelpdeskUrl)
}
//function that takes user to teamviewer ktg site
#objc func kineticRemote(_ sender: Any){
let kineticRemoteUrl = URL(string: "https://get.teamviewer.com/ktgsupport")!
NSWorkspace.shared.open(kineticRemoteUrl)
}
//call kinetic
#objc func kineticHomepage(_ sender: Any){
let url = URL(string: "https://kinetictg.com")!
NSWorkspace.shared.open(url)
}
//function to build menu
func constructMenu(){
let menu = NSMenu()
//section for "Request Support"
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Request Support", action: nil, keyEquivalent:""))
//support ticket
menu.addItem(NSMenuItem(title: "Support Ticket", action:
#selector(AppDelegate.kineticHelpdesk(_:)), keyEquivalent: ""))
//remote support
menu.addItem(NSMenuItem(title: "Remote Support", action:
#selector(AppDelegate.kineticRemote(_:)), keyEquivalent: ""))
//section for "Tools"
menu.addItem(NSMenuItem.separator( ))
menu.addItem(NSMenuItem(title: "Tools", action: nil, keyEquivalent:""))
//start agent installation audit
menu.addItem(NSMenuItem(title: "Take Screenshot", action:
#selector(AppDelegate.TakeScreenshot(_:)), keyEquivalent: ""))
//open self service
menu.addItem(NSMenuItem(title: "Open Self Service", action:
#selector(AppDelegate.kineticSelf(_:)), keyEquivalent: ""))
//Section for "Info"
menu.addItem(NSMenuItem.separator( ))
menu.addItem(NSMenuItem(title: "Info", action: nil, keyEquivalent:""))
//contact info
menu.addItem(NSMenuItem(title: "Kinetic Homepage", action:
#selector(AppDelegate.kineticHomepage(_:)), keyEquivalent: ""))
//quit app
menu.addItem(NSMenuItem(title: "Quit", action:
#selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
statusItem.menu = menu
}
}
Launcher Application:
import Cocoa
//extension variable for launcher to kill launcher
extension Notification.Name {
static let killLauncher = Notification.Name("killLauncher")
}
#NSApplicationMain
class HelperAppDelegate: NSObject {
//terminate object
#objc func terminate(){
NSApp.terminate(nil)
}
}
extension HelperAppDelegate: NSApplicationDelegate{
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
//main app identifier
let mainAppIdentifier = "Kinetic.KTG-Helper"
let runningApps = NSWorkspace.shared.runningApplications
let isRunning = !runningApps.filter { $0.bundleIdentifier == mainAppIdentifier }.isEmpty
//if app is running kill launcher entity and reset status of killlauncher
if !isRunning {
DistributedNotificationCenter.default().addObserver(self, selector: #selector(self.terminate), name: .killLauncher, object: mainAppIdentifier)
let path = Bundle.main.bundlePath as NSString
var components = path.pathComponents
components.removeLast(3)
components.append("MacOS")
components.append("KTG Helper")
let newPath = NSString.path(withComponents: components)
NSWorkspace.shared.launchApplication(newPath)
}
else{
self.terminate()
}
}
//func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//}
}

Track if notification was fired

I have this:
import SwiftUI
import UserNotifications
struct ContentView: View {
var body: some View {
VStack {
Button("Request Permission") {
RequestNotificationsAccess()
}
Button("Schedule Notification") {
ScheduleNotification()
}
}
}
}
func RequestNotificationsAccess() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("All set!")
} else if let error = error {
print(error.localizedDescription)
}
}
}
func ScheduleNotification() {
let content = UNMutableNotificationContent()
content.title = "Title:"
content.subtitle = "some text"
content.sound = UNNotificationSound.default
// show this notification five seconds from now
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
}
It works great but when I click multiple times on the button, the notification appears multiple times, how can I track if it was fired already and if so, don't display anything?
Thanks
ok, to make the previous notification not display, try something like this before you schedule another notification:
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
or keep track of the id and use
removeDeliveredNotifications(withIdentifiers:)"

UNUserNotificationCenter. Notifications are not allowed for this application

I noob in development application on OSX. I want to create app with Share extension. After content loading I want to show Notification, but I getting error "Notifications are not allowed for this application". I don't understand why requestAuthorization method not show dialog windows with permission, and how I can allow application to send notifications.
This is my code:
import Cocoa
import UserNotifications
class ShareViewController: NSViewController {
override func loadView() {
self.view = NSView()
// Insert code here to customize the view
let item = self.extensionContext!.inputItems[0] as! NSExtensionItem
NSLog("Attachment = %#", item.attachments! as NSArray)
showNotification()
let outputItem = NSExtensionItem()
let outputItems = [outputItem]
self.extensionContext!.completeRequest(returningItems: outputItems, completionHandler: nil)
}
func showNotification() -> Void {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.requestAuthorization(options: [.alert, .badge]) {
(granted, error) in
if granted {
print("Yay!")
} else {
print("D'oh") // Print this, not authorized
}
}
let content = UNMutableNotificationContent()
content.title = "Hello"
content.body = "This is example"
content.sound = UNNotificationSound.default
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
notificationCenter.add(request) { (error : Error?) in
if let theError = error {
print(theError) // Print Domain=UNErrorDomain Code=1 "Notifications are not allowed for this application"
}
}
}
}
I don't see anywhere in the documentation that says you can't schedule new Local Notifications from an extension. I do however see an apple support ticket where someone had to deal with this issue, as do I.
Basically this failure is a race-condition. The key is to not call the contentHandler of this extension method:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
until after the completion handler of
notificationCenter.add(request: UNNotificationRequest>, withCompletionHandler: ((Error?) -> Void)
is called. It worked for me. Make sense?
Apple does't allow send Notifications from Share extensions.
Documentation