Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to capture notifications in a WKWebView?

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?

like image 832
BAndonovski Avatar asked Feb 07 '18 20:02

BAndonovski


People also ask

Is WKWebView the same as Safari?

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.

What is the difference between UIWebview and WKWebView?

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.

How do I use WKWebView?

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.

What is a WKWebView?

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.


1 Answers

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.

like image 197
Peter Avatar answered Nov 06 '22 12:11

Peter