Skip to content

Commit

Permalink
Added the WKWebViewJavascriptBridge class
Browse files Browse the repository at this point in the history
  • Loading branch information
lokimeyburg committed Oct 14, 2014
1 parent 5f10f81 commit f39324d
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Example Apps/ExampleApp-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0E8082DB19EDC32300479452 /* WKWebViewJavascriptBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E8082DA19EDC32300479452 /* WKWebViewJavascriptBridge.m */; };
2C1562B5176B9F8400B4AE50 /* WebViewJavascriptBridge.js.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2C1562B4176B9F8400B4AE50 /* WebViewJavascriptBridge.js.txt */; };
2C1562C0176BA63500B4AE50 /* WebViewJavascriptBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1562A9176B9F6200B4AE50 /* WebViewJavascriptBridge.m */; };
2C45CA2C1884AD520002A4E2 /* ExampleAppViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C45CA2B1884AD520002A4E2 /* ExampleAppViewController.m */; };
Expand All @@ -21,6 +22,8 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
0E8082D919EDC32300479452 /* WKWebViewJavascriptBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKWebViewJavascriptBridge.h; sourceTree = "<group>"; };
0E8082DA19EDC32300479452 /* WKWebViewJavascriptBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WKWebViewJavascriptBridge.m; sourceTree = "<group>"; };
2C1562A8176B9F6200B4AE50 /* WebViewJavascriptBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebViewJavascriptBridge.h; sourceTree = "<group>"; };
2C1562A9176B9F6200B4AE50 /* WebViewJavascriptBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WebViewJavascriptBridge.m; sourceTree = "<group>"; };
2C1562B4176B9F8400B4AE50 /* WebViewJavascriptBridge.js.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = WebViewJavascriptBridge.js.txt; sourceTree = "<group>"; };
Expand Down Expand Up @@ -60,6 +63,8 @@
2C1562B4176B9F8400B4AE50 /* WebViewJavascriptBridge.js.txt */,
2C1562A8176B9F6200B4AE50 /* WebViewJavascriptBridge.h */,
2C1562A9176B9F6200B4AE50 /* WebViewJavascriptBridge.m */,
0E8082D919EDC32300479452 /* WKWebViewJavascriptBridge.h */,
0E8082DA19EDC32300479452 /* WKWebViewJavascriptBridge.m */,
);
name = WebViewJavascriptBridge;
path = ../../WebViewJavascriptBridge;
Expand Down Expand Up @@ -184,6 +189,7 @@
buildActionMask = 2147483647;
files = (
2C1562C0176BA63500B4AE50 /* WebViewJavascriptBridge.m in Sources */,
0E8082DB19EDC32300479452 /* WKWebViewJavascriptBridge.m in Sources */,
2C45CA2C1884AD520002A4E2 /* ExampleAppViewController.m in Sources */,
2CA045C217117439006DEE8B /* ExampleAppDelegate.m in Sources */,
2CA045C317117439006DEE8B /* main.m in Sources */,
Expand Down
39 changes: 39 additions & 0 deletions WebViewJavascriptBridge/WKWebViewJavascriptBridge.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// WKWebViewJavascriptBridge.h
//
// Created by Loki Meyburg on 10/15/14.
// Copyright (c) 2014 Loki Meyburg. All rights reserved.
//

#import <Foundation/Foundation.h>

#define kCustomProtocolScheme @"wvjbscheme"
#define kQueueHasMessage @"__WVJB_QUEUE_MESSAGE__"


#if defined(__IPHONE_8_0)
#import <WebKit/WebKit.h>
#define WVJB_PLATFORM_IOS
// #define WVJB_WEBVIEW_TYPE WKWebView
// #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<WKNavigationDelegate>
#endif

typedef void (^WVJBResponseCallback)(id responseData);
typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback);

@interface WKWebViewJavascriptBridge : NSObject<WKNavigationDelegate>

+ (instancetype)bridgeForWebView:(WKWebView*)webView handler:(WVJBHandler)handler;
+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)handler;
+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)handler resourceBundle:(NSBundle*)bundle;
+ (void)enableLogging;

- (void)send:(id)message;
- (void)send:(id)message responseCallback:(WVJBResponseCallback)responseCallback;
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
- (void)reset;

@end
317 changes: 317 additions & 0 deletions WebViewJavascriptBridge/WKWebViewJavascriptBridge.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
//
// WKWebViewJavascriptBridge.m
//
// Created by Loki Meyburg on 10/15/14.
// Copyright (c) 2014 Loki Meyburg. All rights reserved.
//

#import "WKWebViewJavascriptBridge.h"

typedef NSDictionary WVJBMessage;

@implementation WKWebViewJavascriptBridge {
WKWebView* _webView;
id _webViewDelegate;
NSMutableArray* _startupMessageQueue;
NSMutableDictionary* _responseCallbacks;
NSMutableDictionary* _messageHandlers;
long _uniqueId;
WVJBHandler _messageHandler;
NSBundle *_resourceBundle;
NSUInteger _numRequestsLoading;
}

/* API
*****/

static bool logging = false;
+ (void)enableLogging { logging = true; }

+ (instancetype)bridgeForWebView:(WKWebView*)webView handler:(WVJBHandler)handler {
return [self bridgeForWebView:webView webViewDelegate:nil handler:handler];
}

+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)messageHandler {
return [self bridgeForWebView:webView webViewDelegate:webViewDelegate handler:messageHandler resourceBundle:nil];
}

+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)messageHandler resourceBundle:(NSBundle*)bundle
{
WKWebViewJavascriptBridge* bridge = [[WKWebViewJavascriptBridge alloc] init];
[bridge _platformSpecificSetup:webView webViewDelegate:webViewDelegate handler:messageHandler resourceBundle:bundle];
[bridge reset];
return bridge;
}

- (void)send:(id)data {
[self send:data responseCallback:nil];
}

- (void)send:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[self _sendData:data responseCallback:responseCallback handlerName:nil];
}

- (void)callHandler:(NSString *)handlerName {
[self callHandler:handlerName data:nil responseCallback:nil];
}

- (void)callHandler:(NSString *)handlerName data:(id)data {
[self callHandler:handlerName data:data responseCallback:nil];
}

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[self _sendData:data responseCallback:responseCallback handlerName:handlerName];
}

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_messageHandlers[handlerName] = [handler copy];
}

- (void)reset {
_startupMessageQueue = [NSMutableArray array];
_responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}

/* Internals
***********/

- (void)dealloc {
[self _platformSpecificDealloc];

_webView = nil;
_webViewDelegate = nil;
_startupMessageQueue = nil;
_responseCallbacks = nil;
_messageHandlers = nil;
_messageHandler = nil;
}

- (void)_sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];

if (data) {
message[@"data"] = data;
}

if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
_responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}

if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}

- (void)_queueMessage:(WVJBMessage*)message {
if (_startupMessageQueue) {
[_startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}

- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];

NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
});
}
}

- (void)_flushMessageQueue:(NSString *)messageQueueString{
id messages = [self _deserializeMessageJSON:messageQueueString];
if (![messages isKindOfClass:[NSArray class]]) {
NSLog(@"WKWebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messages class], messages);
return;
}
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WKWebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];

NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[_responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}

WVJBHandler handler;
if (message[@"handlerName"]) {
handler = _messageHandlers[message[@"handlerName"]];
} else {
handler = _messageHandler;
}

if (!handler) {
[NSException raise:@"WVJBNoHandlerException" format:@"No handler for message from JS: %@", message];
}

handler(message[@"data"], responseCallback);
}
}
}

- (NSString *)_serializeMessage:(id)message {
return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:0 error:nil] encoding:NSUTF8StringEncoding];
}

- (NSArray*)_deserializeMessageJSON:(NSString *)messageJSON {
return [NSJSONSerialization JSONObjectWithData:[messageJSON dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
}

- (void)_log:(NSString *)action json:(id)json {
if (!logging) { return; }
if (![json isKindOfClass:[NSString class]]) {
json = [self _serializeMessage:json];
}
if ([json length] > 500) {
NSLog(@"WVJB %@: %@ [...]", action, [json substringToIndex:500]);
} else {
NSLog(@"WVJB %@: %@", action, json);
}
}




/* WKWebView Specific Internals
******************************/

- (void) _platformSpecificSetup:(WKWebView*)webView webViewDelegate:(id<WKNavigationDelegate>)webViewDelegate handler:(WVJBHandler)messageHandler resourceBundle:(NSBundle*)bundle{
_messageHandler = messageHandler;
_webView = webView;
_webViewDelegate = webViewDelegate;
_messageHandlers = [NSMutableDictionary dictionary];
_webView.navigationDelegate = self;
_resourceBundle = bundle;
}

- (void) _platformSpecificDealloc {
_webView.navigationDelegate = nil;
}


- (void)WKFlushMessageQueue {
[_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
[self _flushMessageQueue:result];
}];
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
if (webView != _webView) { return; }

_numRequestsLoading--;

if (_numRequestsLoading == 0) {

[webView evaluateJavaScript:@"typeof WebViewJavascriptBridge == \'object\';" completionHandler:^(NSString *result, NSError *error) {
if(![result boolValue]){
NSBundle *bundle = _resourceBundle ? _resourceBundle : [NSBundle mainBundle];
NSString *filePath = [bundle pathForResource:@"WebViewJavascriptBridge.js" ofType:@"txt"];
NSString *js = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[webView evaluateJavaScript:js completionHandler:nil];
}
}];
}

if (_startupMessageQueue) {
for (id queuedMessage in _startupMessageQueue) {
[self _dispatchMessage:queuedMessage];
}
_startupMessageQueue = nil;
}

__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFinishNavigation:)]) {
[strongDelegate webView:webView didFinishNavigation:navigation];
}
}


- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if ([[url scheme] isEqualToString:kCustomProtocolScheme]) {
if ([[url host] isEqualToString:kQueueHasMessage]) {
[self WKFlushMessageQueue];
} else {
NSLog(@"WKWebViewJavascriptBridge: WARNING: Received unknown WKWebViewJavascriptBridge command %@://%@", kCustomProtocolScheme, [url path]);
}
[webView stopLoading];
}

if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
}
}

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
if (webView != _webView) { return; }

_numRequestsLoading++;

__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didStartProvisionalNavigation:)]) {
[strongDelegate webView:webView didStartProvisionalNavigation:navigation];
}
}


- (void)webView:(WKWebView *)webView
didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error {

if (webView != _webView) { return; }

_numRequestsLoading--;

__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFailNavigation:withError:)]) {
[strongDelegate webView:webView didFailNavigation:navigation withError:error];
}
}



@end

0 comments on commit f39324d

Please sign in to comment.