javascriptobjective-ccross-language

Update deprecated webkit objc items to enable native code running within javascript


I'm using legacy webkit based application to generate form on macOS native app (cocoa application written in objective-c)

The following callback to called right before the javascript is loaded to view, and allow current class code (objc) to be used inside the javascript that's about to be loaded.

- (void)webView:(WebView *)webView windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject {
    [windowScriptObject setValue:self forKey:@"app"];
}

Unfortunately, it's been deprecated long ago and I'd like use the updated replacement for webView object which is WKWebView. However, the callback above is delegate method from WebFrameLoadDelegate which is deprecated as well. Perhaps anybody knows how to inject our native code in javascript using WKWebView ?

thanks


Solution

  • This is how you do it with WKWebView. YourWebView is UIView or ViewController class.

    @interface YourWebView () <WKNavigationDelegate, WKScriptMessageHandler>
    
    - (void)injectWSKitScriptInUserContentController:(WKUserContentController*)userContentController;
    
    @end    
    

    in your implementation in -init or -initWithFrame:

    self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
    self.autoresizesSubviews = YES;
    self.wantsLayer = YES;
    
    WKWebViewConfiguration* conf = [[WKWebViewConfiguration alloc] init];
    conf.suppressesIncrementalRendering = NO;
    conf.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll;
    
    WKUserContentController* userContentController = [[WKUserContentController alloc] init];
    [self injectWSKitScriptInUserContentController:userContentController];
    [userContentController addScriptMessageHandler:self name:@"yourscript"];
    conf.userContentController = userContentController;
    
    #ifdef DEBUG
        NSLog(@"Developer Extras Enabled");
        [conf.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
    #endif
    
    WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:conf];
    webView.navigationDelegate = self;
    

    add webView to view or where ever you need it and define a method to inject js.

    -(void)injectWSKitScriptInUserContentController:(WKUserContentController*)userContentController {
        NSBundle* bundle = [NSBundle bundleForClass:[YourWebView class]];
        NSString* scriptLocation = [bundle pathForResource:@"yourscript" ofType:@"js"];
        NSString* scriptSource = [NSString stringWithContentsOfFile:scriptLocation encoding:NSUTF8StringEncoding error:nil];
        WKUserScript* userScript = [[WKUserScript alloc] initWithSource:scriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
        [userContentController addUserScript:userScript];
    }
    

    and then follow the protocols on what else to implement in YourWebView in example something like interaction with your webview, like forward, reload, back, etc..

    and finally you will want to add a javascript file as starting point to your app. "yourscript.js" mentioned above.

    (function() {
    "use strict";
        
        var Events = {
            listeners: { },
    
            gc: function() {
              var events = Object.keys(this.listeners)
              for (var i = events.length - 1; i >= 0; i -= 1) {
                var eventName = events[i],
                    listeners = this.listeners[eventName]
                if (listeners.length === 0) {
                  delete this.listeners[eventName]
                }
              }
            },
            
            once: function(name, listener) {
              if (name in this.listeners) {
                this.listeners[name].push({ oneshot: true, listener: listener })
                return
              }
            
              this.listeners[name] = [
                { oneshot: true, listener: listener },
              ]
            },
            
            on: function(name, listener) {
              if (name in this.listeners) {
                this.listeners[name].push({ listener: listener })
                return
              }
            
              this.listeners[name] = [ { listener: listener } ]
            },
            
            off: function(name, listener) {
              if ( ! (name in this.listeners)) {
                return
              }
            
              var listeners = this.listeners[name]
              for (var i = listeners.length - 1; i >= 0; i -= 1) {
                if (listeners[i].listener === listener) {
                  listeners.splice(i, 1)
                  return
                }
              }
            },
            
            trigger: function(name, arg) {
              if ( ! (name in this.listeners)) {
                return
              }
            
              var event = { stopIteration: false, data: arg }
            
              var listeners = this.listeners[name]
              for (var i = 0; i < listeners.length; i += 1) {
                var listener = listeners[i]
                try {
                  listener.listener(event)
                } catch (e) { }
                if (listener.oneshot) {
                  listeners.splice(i, 1)
                  i -= 1
                }
              }
            
              this.gc()
            },
        }
    
        var ETimeout = new Error('WSKit: configuration timeout'),
            _config = { resolve: null, reject: null, resolved: false }
        
        window.WSKit = {
            configuration: new Promise(function(resolve, reject) {
                _config.resolve = resolve
                _config.reject = reject
            }),
    
            addEventListener: function(name, listener, config) {
                config = config || { }
                
                if (config.oneshot) {
                  Events.once(name, listener)
                } else {
                  Events.on(name, listener)
                }
            },
            removeEventListener: function(name, listener) {
              Events.off(name, listener)
            },
            dispatchEvent: function(name, arg) {
              Events.trigger(name, arg)
            },
        }
    
        setTimeout(function() {
            if ( ! _config.resolved) {
                _config.reject(ETimeout)
            }
        }, 5000)
        
        WSKit.addEventListener('configure', function(ev) {
            _config.resolve(ev.data)
        })
     
        window.webkit.messageHandlers.webscreen.postMessage('obtainconfiguration')
    })();
    

    This should pretty much work