Нет описания

RNCWebView.m 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. /**
  2. * Copyright (c) 2018-present, Infinite Red, Inc.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. */
  7. #import "RNCWebView.h"
  8. // #import <UIKit/UIKit.h>
  9. #import <React/RCTAutoInsetsProtocol.h>
  10. #import <React/RCTConvert.h>
  11. #import <React/RCTEventDispatcher.h>
  12. #import <React/RCTLog.h>
  13. #import <React/RCTUtils.h>
  14. #import <React/RCTView.h>
  15. #import <React/UIView+React.h>
  16. NSString *const RNCJSNavigationScheme = @"react-js-navigation";
  17. static NSString *const kPostMessageHost = @"postMessage";
  18. @interface RNCWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
  19. @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
  20. @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
  21. @property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
  22. @property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
  23. @property (nonatomic, copy) RCTDirectEventBlock onMessage;
  24. @end
  25. @implementation RNCWebView
  26. {
  27. UIWebView *_webView;
  28. NSString *_injectedJavaScript;
  29. }
  30. - (void)dealloc
  31. {
  32. _webView.delegate = nil;
  33. }
  34. - (instancetype)initWithFrame:(CGRect)frame
  35. {
  36. if ((self = [super initWithFrame:frame])) {
  37. super.backgroundColor = [UIColor clearColor];
  38. _automaticallyAdjustContentInsets = YES;
  39. _contentInset = UIEdgeInsetsZero;
  40. _webView = [[UIWebView alloc] initWithFrame:self.bounds];
  41. _webView.delegate = self;
  42. #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
  43. if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
  44. _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  45. }
  46. #endif
  47. [self addSubview:_webView];
  48. }
  49. return self;
  50. }
  51. RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
  52. - (void)goForward
  53. {
  54. [_webView goForward];
  55. }
  56. - (void)goBack
  57. {
  58. [_webView goBack];
  59. }
  60. - (void)reload
  61. {
  62. NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
  63. if (request.URL && !_webView.request.URL.absoluteString.length) {
  64. [_webView loadRequest:request];
  65. }
  66. else {
  67. [_webView reload];
  68. }
  69. }
  70. - (void)stopLoading
  71. {
  72. [_webView stopLoading];
  73. }
  74. - (void)postMessage:(NSString *)message
  75. {
  76. NSDictionary *eventInitDict = @{
  77. @"data": message,
  78. };
  79. NSString *source = [NSString
  80. stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
  81. RCTJSONStringify(eventInitDict, NULL)
  82. ];
  83. [_webView stringByEvaluatingJavaScriptFromString:source];
  84. }
  85. - (void)injectJavaScript:(NSString *)script
  86. {
  87. [_webView stringByEvaluatingJavaScriptFromString:script];
  88. }
  89. - (void)setSource:(NSDictionary *)source
  90. {
  91. if (![_source isEqualToDictionary:source]) {
  92. _source = [source copy];
  93. // Check for a static html source first
  94. NSString *html = [RCTConvert NSString:source[@"html"]];
  95. if (html) {
  96. NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]];
  97. if (!baseURL) {
  98. baseURL = [NSURL URLWithString:@"about:blank"];
  99. }
  100. [_webView loadHTMLString:html baseURL:baseURL];
  101. return;
  102. }
  103. NSURLRequest *request = [RCTConvert NSURLRequest:source];
  104. // Because of the way React works, as pages redirect, we actually end up
  105. // passing the redirect urls back here, so we ignore them if trying to load
  106. // the same url. We'll expose a call to 'reload' to allow a user to load
  107. // the existing page.
  108. if ([request.URL isEqual:_webView.request.URL]) {
  109. return;
  110. }
  111. if (!request.URL) {
  112. // Clear the webview
  113. [_webView loadHTMLString:@"" baseURL:nil];
  114. return;
  115. }
  116. [_webView loadRequest:request];
  117. }
  118. }
  119. - (void)layoutSubviews
  120. {
  121. [super layoutSubviews];
  122. _webView.frame = self.bounds;
  123. }
  124. - (void)setContentInset:(UIEdgeInsets)contentInset
  125. {
  126. _contentInset = contentInset;
  127. [RCTView autoAdjustInsetsForView:self
  128. withScrollView:_webView.scrollView
  129. updateOffset:NO];
  130. }
  131. - (void)setScalesPageToFit:(BOOL)scalesPageToFit
  132. {
  133. if (_webView.scalesPageToFit != scalesPageToFit) {
  134. _webView.scalesPageToFit = scalesPageToFit;
  135. [_webView reload];
  136. }
  137. }
  138. - (BOOL)scalesPageToFit
  139. {
  140. return _webView.scalesPageToFit;
  141. }
  142. - (void)setBackgroundColor:(UIColor *)backgroundColor
  143. {
  144. CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
  145. self.opaque = _webView.opaque = (alpha == 1.0);
  146. _webView.backgroundColor = backgroundColor;
  147. }
  148. - (UIColor *)backgroundColor
  149. {
  150. return _webView.backgroundColor;
  151. }
  152. - (NSMutableDictionary<NSString *, id> *)baseEvent
  153. {
  154. NSMutableDictionary<NSString *, id> *event = [[NSMutableDictionary alloc] initWithDictionary:@{
  155. @"url": _webView.request.URL.absoluteString ?: @"",
  156. @"loading" : @(_webView.loading),
  157. @"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"],
  158. @"canGoBack": @(_webView.canGoBack),
  159. @"canGoForward" : @(_webView.canGoForward),
  160. }];
  161. return event;
  162. }
  163. - (void)refreshContentInset
  164. {
  165. [RCTView autoAdjustInsetsForView:self
  166. withScrollView:_webView.scrollView
  167. updateOffset:YES];
  168. }
  169. #pragma mark - UIWebViewDelegate methods
  170. - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
  171. navigationType:(UIWebViewNavigationType)navigationType
  172. {
  173. BOOL isJSNavigation = [request.URL.scheme isEqualToString:RNCJSNavigationScheme];
  174. static NSDictionary<NSNumber *, NSString *> *navigationTypes;
  175. static dispatch_once_t onceToken;
  176. dispatch_once(&onceToken, ^{
  177. navigationTypes = @{
  178. @(UIWebViewNavigationTypeLinkClicked): @"click",
  179. @(UIWebViewNavigationTypeFormSubmitted): @"formsubmit",
  180. @(UIWebViewNavigationTypeBackForward): @"backforward",
  181. @(UIWebViewNavigationTypeReload): @"reload",
  182. @(UIWebViewNavigationTypeFormResubmitted): @"formresubmit",
  183. @(UIWebViewNavigationTypeOther): @"other",
  184. };
  185. });
  186. // skip this for the JS Navigation handler
  187. if (!isJSNavigation && _onShouldStartLoadWithRequest) {
  188. NSMutableDictionary<NSString *, id> *event = [self baseEvent];
  189. [event addEntriesFromDictionary: @{
  190. @"url": (request.URL).absoluteString,
  191. @"navigationType": navigationTypes[@(navigationType)]
  192. }];
  193. if (![self.delegate webView:self
  194. shouldStartLoadForRequest:event
  195. withCallback:_onShouldStartLoadWithRequest]) {
  196. return NO;
  197. }
  198. }
  199. if (_onLoadingStart) {
  200. // We have this check to filter out iframe requests and whatnot
  201. BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
  202. if (isTopFrame) {
  203. NSMutableDictionary<NSString *, id> *event = [self baseEvent];
  204. [event addEntriesFromDictionary: @{
  205. @"url": (request.URL).absoluteString,
  206. @"navigationType": navigationTypes[@(navigationType)]
  207. }];
  208. _onLoadingStart(event);
  209. }
  210. }
  211. if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
  212. NSString *data = request.URL.query;
  213. data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
  214. data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  215. NSMutableDictionary<NSString *, id> *event = [self baseEvent];
  216. [event addEntriesFromDictionary: @{
  217. @"data": data,
  218. }];
  219. NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));";
  220. [_webView stringByEvaluatingJavaScriptFromString:source];
  221. _onMessage(event);
  222. }
  223. // JS Navigation handler
  224. return !isJSNavigation;
  225. }
  226. - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
  227. {
  228. if (_onLoadingError) {
  229. if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
  230. // NSURLErrorCancelled is reported when a page has a redirect OR if you load
  231. // a new URL in the WebView before the previous one came back. We can just
  232. // ignore these since they aren't real errors.
  233. // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
  234. return;
  235. }
  236. if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
  237. // Error code 102 "Frame load interrupted" is raised by the UIWebView if
  238. // its delegate returns FALSE from webView:shouldStartLoadWithRequest:navigationType
  239. // when the URL is from an http redirect. This is a common pattern when
  240. // implementing OAuth with a WebView.
  241. return;
  242. }
  243. NSMutableDictionary<NSString *, id> *event = [self baseEvent];
  244. [event addEntriesFromDictionary:@{
  245. @"domain": error.domain,
  246. @"code": @(error.code),
  247. @"description": error.localizedDescription,
  248. }];
  249. _onLoadingError(event);
  250. }
  251. }
  252. - (void)webViewDidFinishLoad:(UIWebView *)webView
  253. {
  254. if (_messagingEnabled) {
  255. #if RCT_DEV
  256. // See isNative in lodash
  257. NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
  258. BOOL postMessageIsNative = [
  259. [webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
  260. isEqualToString:@"true"
  261. ];
  262. if (!postMessageIsNative) {
  263. RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
  264. }
  265. #endif
  266. NSString *source = [NSString stringWithFormat:
  267. @"(function() {"
  268. "window.originalPostMessage = window.postMessage;"
  269. "var messageQueue = [];"
  270. "var messagePending = false;"
  271. "function processQueue() {"
  272. "if (!messageQueue.length || messagePending) return;"
  273. "messagePending = true;"
  274. "window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
  275. "}"
  276. "window.postMessage = function(data) {"
  277. "messageQueue.push(String(data));"
  278. "processQueue();"
  279. "};"
  280. "document.addEventListener('message:received', function(e) {"
  281. "messagePending = false;"
  282. "processQueue();"
  283. "});"
  284. "})();", RNCJSNavigationScheme, kPostMessageHost
  285. ];
  286. [webView stringByEvaluatingJavaScriptFromString:source];
  287. }
  288. if (_injectedJavaScript != nil) {
  289. NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
  290. NSMutableDictionary<NSString *, id> *event = [self baseEvent];
  291. event[@"jsEvaluationValue"] = jsEvaluationValue;
  292. _onLoadingFinish(event);
  293. }
  294. // we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
  295. else if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) {
  296. _onLoadingFinish([self baseEvent]);
  297. }
  298. }
  299. @end