I'm working on a macOS desktop app in Swift 4.
It has a WKWebView which loads up a web page that sends notifications.
None of the notifications are shown by default and there's also no permission request.
I need a way to show the notifications and intercept them, so that I can show a counter.
Any idea how to achieve this?
WKWebView. WKWebView was introduced in iOS 8 allowing app developers to implement a web browsing interface similar to that of mobile Safari. This is due, in part, to the fact that WKWebView uses the Nitro Javascript engine, the same engine used by mobile Safari.
Unlike UIWebView, which does not support server authentication challenges, WKWebView does. In practical terms, this means that when using WKWebView, you can enter site credentials for password-protected websites.
Here's how: Open the XIB or Storyboard you want to add the web view to in Interface Builder. Find the web view or WKWebView in the Object Library at the bottom-left of Interface Builder. Drag-and-drop a WKWebView object from the Object Library to your view controller's canvas, and adjust its size and position.
A WKWebView object is a platform-native view that you use to incorporate web content seamlessly into your app's UI. A web view supports a full web-browsing experience, and presents HTML, CSS, and JavaScript content alongside your app's native views.
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.
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.
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.
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.
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.
I created an example project available on GitHub which renders the whole picture.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With