Skip to content

Commit f39324d

Browse files
committed
Added the WKWebViewJavascriptBridge class
1 parent 5f10f81 commit f39324d

File tree

3 files changed

+362
-0
lines changed

3 files changed

+362
-0
lines changed

Example Apps/ExampleApp-iOS.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

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

2324
/* Begin PBXFileReference section */
25+
0E8082D919EDC32300479452 /* WKWebViewJavascriptBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKWebViewJavascriptBridge.h; sourceTree = "<group>"; };
26+
0E8082DA19EDC32300479452 /* WKWebViewJavascriptBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WKWebViewJavascriptBridge.m; sourceTree = "<group>"; };
2427
2C1562A8176B9F6200B4AE50 /* WebViewJavascriptBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebViewJavascriptBridge.h; sourceTree = "<group>"; };
2528
2C1562A9176B9F6200B4AE50 /* WebViewJavascriptBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WebViewJavascriptBridge.m; sourceTree = "<group>"; };
2629
2C1562B4176B9F8400B4AE50 /* WebViewJavascriptBridge.js.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = WebViewJavascriptBridge.js.txt; sourceTree = "<group>"; };
@@ -60,6 +63,8 @@
6063
2C1562B4176B9F8400B4AE50 /* WebViewJavascriptBridge.js.txt */,
6164
2C1562A8176B9F6200B4AE50 /* WebViewJavascriptBridge.h */,
6265
2C1562A9176B9F6200B4AE50 /* WebViewJavascriptBridge.m */,
66+
0E8082D919EDC32300479452 /* WKWebViewJavascriptBridge.h */,
67+
0E8082DA19EDC32300479452 /* WKWebViewJavascriptBridge.m */,
6368
);
6469
name = WebViewJavascriptBridge;
6570
path = ../../WebViewJavascriptBridge;
@@ -184,6 +189,7 @@
184189
buildActionMask = 2147483647;
185190
files = (
186191
2C1562C0176BA63500B4AE50 /* WebViewJavascriptBridge.m in Sources */,
192+
0E8082DB19EDC32300479452 /* WKWebViewJavascriptBridge.m in Sources */,
187193
2C45CA2C1884AD520002A4E2 /* ExampleAppViewController.m in Sources */,
188194
2CA045C217117439006DEE8B /* ExampleAppDelegate.m in Sources */,
189195
2CA045C317117439006DEE8B /* main.m in Sources */,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// WKWebViewJavascriptBridge.h
3+
//
4+
// Created by Loki Meyburg on 10/15/14.
5+
// Copyright (c) 2014 Loki Meyburg. All rights reserved.
6+
//
7+
8+
#import <Foundation/Foundation.h>
9+
10+
#define kCustomProtocolScheme @"wvjbscheme"
11+
#define kQueueHasMessage @"__WVJB_QUEUE_MESSAGE__"
12+
13+
14+
#if defined(__IPHONE_8_0)
15+
#import <WebKit/WebKit.h>
16+
#define WVJB_PLATFORM_IOS
17+
// #define WVJB_WEBVIEW_TYPE WKWebView
18+
// #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<WKNavigationDelegate>
19+
#endif
20+
21+
typedef void (^WVJBResponseCallback)(id responseData);
22+
typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback);
23+
24+
@interface WKWebViewJavascriptBridge : NSObject<WKNavigationDelegate>
25+
26+
+ (instancetype)bridgeForWebView:(WKWebView*)webView handler:(WVJBHandler)handler;
27+
+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)handler;
28+
+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)handler resourceBundle:(NSBundle*)bundle;
29+
+ (void)enableLogging;
30+
31+
- (void)send:(id)message;
32+
- (void)send:(id)message responseCallback:(WVJBResponseCallback)responseCallback;
33+
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
34+
- (void)callHandler:(NSString*)handlerName;
35+
- (void)callHandler:(NSString*)handlerName data:(id)data;
36+
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
37+
- (void)reset;
38+
39+
@end
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
//
2+
// WKWebViewJavascriptBridge.m
3+
//
4+
// Created by Loki Meyburg on 10/15/14.
5+
// Copyright (c) 2014 Loki Meyburg. All rights reserved.
6+
//
7+
8+
#import "WKWebViewJavascriptBridge.h"
9+
10+
typedef NSDictionary WVJBMessage;
11+
12+
@implementation WKWebViewJavascriptBridge {
13+
WKWebView* _webView;
14+
id _webViewDelegate;
15+
NSMutableArray* _startupMessageQueue;
16+
NSMutableDictionary* _responseCallbacks;
17+
NSMutableDictionary* _messageHandlers;
18+
long _uniqueId;
19+
WVJBHandler _messageHandler;
20+
NSBundle *_resourceBundle;
21+
NSUInteger _numRequestsLoading;
22+
}
23+
24+
/* API
25+
*****/
26+
27+
static bool logging = false;
28+
+ (void)enableLogging { logging = true; }
29+
30+
+ (instancetype)bridgeForWebView:(WKWebView*)webView handler:(WVJBHandler)handler {
31+
return [self bridgeForWebView:webView webViewDelegate:nil handler:handler];
32+
}
33+
34+
+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)messageHandler {
35+
return [self bridgeForWebView:webView webViewDelegate:webViewDelegate handler:messageHandler resourceBundle:nil];
36+
}
37+
38+
+ (instancetype)bridgeForWebView:(WKWebView*)webView webViewDelegate:(NSObject<WKNavigationDelegate>*)webViewDelegate handler:(WVJBHandler)messageHandler resourceBundle:(NSBundle*)bundle
39+
{
40+
WKWebViewJavascriptBridge* bridge = [[WKWebViewJavascriptBridge alloc] init];
41+
[bridge _platformSpecificSetup:webView webViewDelegate:webViewDelegate handler:messageHandler resourceBundle:bundle];
42+
[bridge reset];
43+
return bridge;
44+
}
45+
46+
- (void)send:(id)data {
47+
[self send:data responseCallback:nil];
48+
}
49+
50+
- (void)send:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
51+
[self _sendData:data responseCallback:responseCallback handlerName:nil];
52+
}
53+
54+
- (void)callHandler:(NSString *)handlerName {
55+
[self callHandler:handlerName data:nil responseCallback:nil];
56+
}
57+
58+
- (void)callHandler:(NSString *)handlerName data:(id)data {
59+
[self callHandler:handlerName data:data responseCallback:nil];
60+
}
61+
62+
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
63+
[self _sendData:data responseCallback:responseCallback handlerName:handlerName];
64+
}
65+
66+
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
67+
_messageHandlers[handlerName] = [handler copy];
68+
}
69+
70+
- (void)reset {
71+
_startupMessageQueue = [NSMutableArray array];
72+
_responseCallbacks = [NSMutableDictionary dictionary];
73+
_uniqueId = 0;
74+
}
75+
76+
/* Internals
77+
***********/
78+
79+
- (void)dealloc {
80+
[self _platformSpecificDealloc];
81+
82+
_webView = nil;
83+
_webViewDelegate = nil;
84+
_startupMessageQueue = nil;
85+
_responseCallbacks = nil;
86+
_messageHandlers = nil;
87+
_messageHandler = nil;
88+
}
89+
90+
- (void)_sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
91+
NSMutableDictionary* message = [NSMutableDictionary dictionary];
92+
93+
if (data) {
94+
message[@"data"] = data;
95+
}
96+
97+
if (responseCallback) {
98+
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
99+
_responseCallbacks[callbackId] = [responseCallback copy];
100+
message[@"callbackId"] = callbackId;
101+
}
102+
103+
if (handlerName) {
104+
message[@"handlerName"] = handlerName;
105+
}
106+
[self _queueMessage:message];
107+
}
108+
109+
- (void)_queueMessage:(WVJBMessage*)message {
110+
if (_startupMessageQueue) {
111+
[_startupMessageQueue addObject:message];
112+
} else {
113+
[self _dispatchMessage:message];
114+
}
115+
}
116+
117+
- (void)_dispatchMessage:(WVJBMessage*)message {
118+
NSString *messageJSON = [self _serializeMessage:message];
119+
[self _log:@"SEND" json:messageJSON];
120+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
121+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
122+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
123+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
124+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
125+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
126+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
127+
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
128+
129+
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
130+
if ([[NSThread currentThread] isMainThread]) {
131+
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
132+
} else {
133+
dispatch_sync(dispatch_get_main_queue(), ^{
134+
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
135+
});
136+
}
137+
}
138+
139+
- (void)_flushMessageQueue:(NSString *)messageQueueString{
140+
id messages = [self _deserializeMessageJSON:messageQueueString];
141+
if (![messages isKindOfClass:[NSArray class]]) {
142+
NSLog(@"WKWebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messages class], messages);
143+
return;
144+
}
145+
for (WVJBMessage* message in messages) {
146+
if (![message isKindOfClass:[WVJBMessage class]]) {
147+
NSLog(@"WKWebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
148+
continue;
149+
}
150+
[self _log:@"RCVD" json:message];
151+
152+
NSString* responseId = message[@"responseId"];
153+
if (responseId) {
154+
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
155+
responseCallback(message[@"responseData"]);
156+
[_responseCallbacks removeObjectForKey:responseId];
157+
} else {
158+
WVJBResponseCallback responseCallback = NULL;
159+
NSString* callbackId = message[@"callbackId"];
160+
if (callbackId) {
161+
responseCallback = ^(id responseData) {
162+
if (responseData == nil) {
163+
responseData = [NSNull null];
164+
}
165+
166+
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
167+
[self _queueMessage:msg];
168+
};
169+
} else {
170+
responseCallback = ^(id ignoreResponseData) {
171+
// Do nothing
172+
};
173+
}
174+
175+
WVJBHandler handler;
176+
if (message[@"handlerName"]) {
177+
handler = _messageHandlers[message[@"handlerName"]];
178+
} else {
179+
handler = _messageHandler;
180+
}
181+
182+
if (!handler) {
183+
[NSException raise:@"WVJBNoHandlerException" format:@"No handler for message from JS: %@", message];
184+
}
185+
186+
handler(message[@"data"], responseCallback);
187+
}
188+
}
189+
}
190+
191+
- (NSString *)_serializeMessage:(id)message {
192+
return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:0 error:nil] encoding:NSUTF8StringEncoding];
193+
}
194+
195+
- (NSArray*)_deserializeMessageJSON:(NSString *)messageJSON {
196+
return [NSJSONSerialization JSONObjectWithData:[messageJSON dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
197+
}
198+
199+
- (void)_log:(NSString *)action json:(id)json {
200+
if (!logging) { return; }
201+
if (![json isKindOfClass:[NSString class]]) {
202+
json = [self _serializeMessage:json];
203+
}
204+
if ([json length] > 500) {
205+
NSLog(@"WVJB %@: %@ [...]", action, [json substringToIndex:500]);
206+
} else {
207+
NSLog(@"WVJB %@: %@", action, json);
208+
}
209+
}
210+
211+
212+
213+
214+
/* WKWebView Specific Internals
215+
******************************/
216+
217+
- (void) _platformSpecificSetup:(WKWebView*)webView webViewDelegate:(id<WKNavigationDelegate>)webViewDelegate handler:(WVJBHandler)messageHandler resourceBundle:(NSBundle*)bundle{
218+
_messageHandler = messageHandler;
219+
_webView = webView;
220+
_webViewDelegate = webViewDelegate;
221+
_messageHandlers = [NSMutableDictionary dictionary];
222+
_webView.navigationDelegate = self;
223+
_resourceBundle = bundle;
224+
}
225+
226+
- (void) _platformSpecificDealloc {
227+
_webView.navigationDelegate = nil;
228+
}
229+
230+
231+
- (void)WKFlushMessageQueue {
232+
[_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
233+
[self _flushMessageQueue:result];
234+
}];
235+
}
236+
237+
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
238+
{
239+
if (webView != _webView) { return; }
240+
241+
_numRequestsLoading--;
242+
243+
if (_numRequestsLoading == 0) {
244+
245+
[webView evaluateJavaScript:@"typeof WebViewJavascriptBridge == \'object\';" completionHandler:^(NSString *result, NSError *error) {
246+
if(![result boolValue]){
247+
NSBundle *bundle = _resourceBundle ? _resourceBundle : [NSBundle mainBundle];
248+
NSString *filePath = [bundle pathForResource:@"WebViewJavascriptBridge.js" ofType:@"txt"];
249+
NSString *js = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
250+
[webView evaluateJavaScript:js completionHandler:nil];
251+
}
252+
}];
253+
}
254+
255+
if (_startupMessageQueue) {
256+
for (id queuedMessage in _startupMessageQueue) {
257+
[self _dispatchMessage:queuedMessage];
258+
}
259+
_startupMessageQueue = nil;
260+
}
261+
262+
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
263+
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFinishNavigation:)]) {
264+
[strongDelegate webView:webView didFinishNavigation:navigation];
265+
}
266+
}
267+
268+
269+
- (void)webView:(WKWebView *)webView
270+
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
271+
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
272+
if (webView != _webView) { return; }
273+
NSURL *url = navigationAction.request.URL;
274+
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
275+
if ([[url scheme] isEqualToString:kCustomProtocolScheme]) {
276+
if ([[url host] isEqualToString:kQueueHasMessage]) {
277+
[self WKFlushMessageQueue];
278+
} else {
279+
NSLog(@"WKWebViewJavascriptBridge: WARNING: Received unknown WKWebViewJavascriptBridge command %@://%@", kCustomProtocolScheme, [url path]);
280+
}
281+
[webView stopLoading];
282+
}
283+
284+
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
285+
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
286+
}
287+
}
288+
289+
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
290+
if (webView != _webView) { return; }
291+
292+
_numRequestsLoading++;
293+
294+
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
295+
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didStartProvisionalNavigation:)]) {
296+
[strongDelegate webView:webView didStartProvisionalNavigation:navigation];
297+
}
298+
}
299+
300+
301+
- (void)webView:(WKWebView *)webView
302+
didFailNavigation:(WKNavigation *)navigation
303+
withError:(NSError *)error {
304+
305+
if (webView != _webView) { return; }
306+
307+
_numRequestsLoading--;
308+
309+
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
310+
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFailNavigation:withError:)]) {
311+
[strongDelegate webView:webView didFailNavigation:navigation withError:error];
312+
}
313+
}
314+
315+
316+
317+
@end

0 commit comments

Comments
 (0)