0

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

Zohar81
  • 4,554
  • 5
  • 29
  • 82
  • Not sure I understand the question, are you asking how to migrate from legacy to current? – rexfordkelly Jul 09 '20 at 22:06
  • or do you want objc defined objects to be available in javascript? The Apple documentation on WKWebView is pretty, even with example listing. – Ol Sen Jul 09 '20 at 22:21

1 Answers1

1

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

Ol Sen
  • 3,163
  • 2
  • 21
  • 30
  • Hi and that's for the help. I've defined webViewCls which derives from `NSView< >` and tried to use it on my AppDelegate, but without success so far, perhaps you can reference me to a full working example ? thanks ! – Zohar81 Jul 11 '20 at 11:50
  • hmm AppDelegate should be not involved here. Your NSViewController starts/controls your NSView, your NSView initiation is creating a WKWebView *webview = [[WKWebView alloc] init...] and setting the webview.delegate to self. Where self is your NSView and should contain the protocol methods for the delegate. – Ol Sen Jul 11 '20 at 14:17