No Description

RNCUIWebView.m 10KB

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