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