Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
837 views
in Technique[技术] by (71.8m points)

swift - 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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...