I was facing the same challenge and solved it by injecting a script (WKUserScript
) which overrides the web notification API with a custom implementation that leverages the WKUserContentController
to send messages to the native app code which posts the final notifications in the end.
Setting up WKWebView
Programmatic creation of a WKWebView
is necessary to use a custom WKWebViewConfiguration
as far as I know. Creating a new macOS app project I extend my viewDidLoad
in the ViewController
function like this:
override func viewDidLoad() {
super.viewDidLoad()
let userScriptURL = Bundle.main.url(forResource: "UserScript", withExtension: "js")!
let userScriptCode = try! String(contentsOf: userScriptURL)
let userScript = WKUserScript(source: userScriptCode, injectionTime: .atDocumentStart, forMainFrameOnly: false)
let configuration = WKWebViewConfiguration()
configuration.userContentController.addUserScript(userScript)
configuration.userContentController.add(self, name: "notify")
let documentURL = Bundle.main.url(forResource: "Document", withExtension: "html")!
let webView = WKWebView(frame: view.frame, configuration: configuration)
webView.loadFileURL(documentURL, allowingReadAccessTo: documentURL)
view.addSubview(webView)
}
First I load the user script from the app bundle and add it to the user content controller. I also add a message handler called notify
which can be used to phone back from the JavaScript context to native code. At the end I load an example HTML document from the app bundle and present the web view using the whole area available in the window.
Overriding the Notification API
This is the injected user script and a partial override of the web Notification API. It is sufficient to handle the typical notification permission request process and posting of notifications in scope of this generic question.
/**
* Incomplete Notification API override to enable native notifications.
*/
class NotificationOverride {
// Grant permission by default to keep this example simple.
// Safari 13 does not support class fields yet, so a static getter must be used.
static get permission() {
return "granted";
}
// Safari 13 still uses callbacks instead of promises.
static requestPermission (callback) {
callback("granted");
}
// Forward the notification text to the native app through the script message handler.
constructor (messageText) {
window.webkit.messageHandlers.notify.postMessage(messageText);
}
}
// Override the global browser notification object.
window.Notification = NotificationOverride;
Every time a new notification is created in the JavaScript context, the user content controller message handler is invoked.
Handling the Script Message
The ViewController
(or whatever else should handle the script messages) needs to conform to WKScriptMessageHandler
and implement the following function to handle invocations:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let content = UNMutableNotificationContent()
content.title = "WKWebView Notification Example"
content.body = message.body as! String
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: nil)
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in
if error != nil {
NSLog(error.debugDescription)
}
}
}
The whole implementation is about the creation of a local, native notification in macOS. It does not work yet without additional effort, though.
App Delegate Adjustments
Before a macOS app is allowed to post notifications, it must request the permissions to do so, like a website.
func applicationDidFinishLaunching(_ aNotification: Notification) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (granted, error) in
// Enable or disable features based on authorization.
}
}
If notifications should be presented while the app is in the foreground, the app delegate must be extended further to conform to UNUserNotificationCenterDelegate
and implement:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler(UNNotificationPresentationOptions.alert)
}
which requires the delegate assignment in applicationDidFinishLaunching(_:)
:
UNUserNotificationCenter.current().delegate = self
The UNNotificationPresentationOptions
may vary according to your requirements.
Reference
I created an example project available on GitHub which renders the whole picture.