RCTView.m 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  1. /*
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  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 "RCTView.h"
  8. #import "RCTAutoInsetsProtocol.h"
  9. #import "RCTBorderDrawing.h"
  10. #import "RCTConvert.h"
  11. #import "RCTI18nUtil.h"
  12. #import "RCTLog.h"
  13. #import "RCTUtils.h"
  14. #import "UIView+React.h"
  15. UIAccessibilityTraits const SwitchAccessibilityTrait = 0x20000000000001;
  16. @implementation UIView (RCTViewUnmounting)
  17. - (void)react_remountAllSubviews
  18. {
  19. // Normal views don't support unmounting, so all
  20. // this does is forward message to our subviews,
  21. // in case any of those do support it
  22. for (UIView *subview in self.subviews) {
  23. [subview react_remountAllSubviews];
  24. }
  25. }
  26. - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
  27. {
  28. // Even though we don't support subview unmounting
  29. // we do support clipsToBounds, so if that's enabled
  30. // we'll update the clipping
  31. if (self.clipsToBounds && self.subviews.count > 0) {
  32. clipRect = [clipView convertRect:clipRect toView:self];
  33. clipRect = CGRectIntersection(clipRect, self.bounds);
  34. clipView = self;
  35. }
  36. // Normal views don't support unmounting, so all
  37. // this does is forward message to our subviews,
  38. // in case any of those do support it
  39. for (UIView *subview in self.subviews) {
  40. [subview react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
  41. }
  42. }
  43. - (UIView *)react_findClipView
  44. {
  45. UIView *testView = self;
  46. UIView *clipView = nil;
  47. CGRect clipRect = self.bounds;
  48. // We will only look for a clipping view up the view hierarchy until we hit the root view.
  49. while (testView) {
  50. if (testView.clipsToBounds) {
  51. if (clipView) {
  52. CGRect testRect = [clipView convertRect:clipRect toView:testView];
  53. if (!CGRectContainsRect(testView.bounds, testRect)) {
  54. clipView = testView;
  55. clipRect = CGRectIntersection(testView.bounds, testRect);
  56. }
  57. } else {
  58. clipView = testView;
  59. clipRect = [self convertRect:self.bounds toView:clipView];
  60. }
  61. }
  62. if ([testView isReactRootView]) {
  63. break;
  64. }
  65. testView = testView.superview;
  66. }
  67. return clipView ?: self.window;
  68. }
  69. @end
  70. static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
  71. {
  72. NSMutableString *str = [NSMutableString stringWithString:@""];
  73. for (UIView *subview in view.subviews) {
  74. NSString *label = subview.accessibilityLabel;
  75. if (!label) {
  76. label = RCTRecursiveAccessibilityLabel(subview);
  77. }
  78. if (label && label.length > 0) {
  79. if (str.length > 0) {
  80. [str appendString:@" "];
  81. }
  82. [str appendString:label];
  83. }
  84. }
  85. return str.length == 0 ? nil : str;
  86. }
  87. @implementation RCTView {
  88. UIColor *_backgroundColor;
  89. NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsNameMap;
  90. NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsLabelMap;
  91. }
  92. - (instancetype)initWithFrame:(CGRect)frame
  93. {
  94. if ((self = [super initWithFrame:frame])) {
  95. _borderWidth = -1;
  96. _borderTopWidth = -1;
  97. _borderRightWidth = -1;
  98. _borderBottomWidth = -1;
  99. _borderLeftWidth = -1;
  100. _borderStartWidth = -1;
  101. _borderEndWidth = -1;
  102. _borderTopLeftRadius = -1;
  103. _borderTopRightRadius = -1;
  104. _borderTopStartRadius = -1;
  105. _borderTopEndRadius = -1;
  106. _borderBottomLeftRadius = -1;
  107. _borderBottomRightRadius = -1;
  108. _borderBottomStartRadius = -1;
  109. _borderBottomEndRadius = -1;
  110. _borderStyle = RCTBorderStyleSolid;
  111. _hitTestEdgeInsets = UIEdgeInsetsZero;
  112. _backgroundColor = super.backgroundColor;
  113. }
  114. return self;
  115. }
  116. RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : unused)
  117. - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
  118. {
  119. if (_reactLayoutDirection != layoutDirection) {
  120. _reactLayoutDirection = layoutDirection;
  121. [self.layer setNeedsDisplay];
  122. }
  123. if ([self respondsToSelector:@selector(setSemanticContentAttribute:)]) {
  124. self.semanticContentAttribute = layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight
  125. ? UISemanticContentAttributeForceLeftToRight
  126. : UISemanticContentAttributeForceRightToLeft;
  127. }
  128. }
  129. - (NSString *)accessibilityLabel
  130. {
  131. NSString *label = super.accessibilityLabel;
  132. if (label) {
  133. return label;
  134. }
  135. return RCTRecursiveAccessibilityLabel(self);
  136. }
  137. - (NSArray<UIAccessibilityCustomAction *> *)accessibilityCustomActions
  138. {
  139. if (!self.accessibilityActions.count) {
  140. return nil;
  141. }
  142. accessibilityActionsNameMap = [[NSMutableDictionary alloc] init];
  143. accessibilityActionsLabelMap = [[NSMutableDictionary alloc] init];
  144. NSMutableArray *actions = [NSMutableArray array];
  145. for (NSDictionary *action in self.accessibilityActions) {
  146. if (action[@"name"]) {
  147. accessibilityActionsNameMap[action[@"name"]] = action;
  148. }
  149. if (action[@"label"]) {
  150. accessibilityActionsLabelMap[action[@"label"]] = action;
  151. [actions addObject:[[UIAccessibilityCustomAction alloc]
  152. initWithName:action[@"label"]
  153. target:self
  154. selector:@selector(didActivateAccessibilityCustomAction:)]];
  155. }
  156. }
  157. return [actions copy];
  158. }
  159. - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
  160. {
  161. if (!_onAccessibilityAction || !accessibilityActionsLabelMap) {
  162. return NO;
  163. }
  164. // iOS defines the name as the localized label, so use our map to convert this back to the non-localized action namne
  165. // when passing to JS. This allows for standard action names across platforms.
  166. NSDictionary *actionObject = accessibilityActionsLabelMap[action.name];
  167. if (actionObject) {
  168. _onAccessibilityAction(@{@"actionName" : actionObject[@"name"], @"actionTarget" : self.reactTag});
  169. }
  170. return YES;
  171. }
  172. - (NSString *)accessibilityValue
  173. {
  174. static dispatch_once_t onceToken;
  175. static NSDictionary<NSString *, NSString *> *rolesAndStatesDescription = nil;
  176. dispatch_once(&onceToken, ^{
  177. NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"AccessibilityResources" ofType:@"bundle"];
  178. NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
  179. if (bundle) {
  180. NSURL *url = [bundle URLForResource:@"Localizable" withExtension:@"strings"];
  181. if (@available(iOS 11.0, *)) {
  182. rolesAndStatesDescription = [NSDictionary dictionaryWithContentsOfURL:url error:nil];
  183. } else {
  184. // Fallback on earlier versions
  185. rolesAndStatesDescription = [NSDictionary dictionaryWithContentsOfURL:url];
  186. }
  187. }
  188. if (rolesAndStatesDescription == nil) {
  189. NSLog(@"Cannot load localized accessibility strings.");
  190. rolesAndStatesDescription = @{
  191. @"alert" : @"alert",
  192. @"checkbox" : @"checkbox",
  193. @"combobox" : @"combo box",
  194. @"menu" : @"menu",
  195. @"menubar" : @"menu bar",
  196. @"menuitem" : @"menu item",
  197. @"progressbar" : @"progress bar",
  198. @"radio" : @"radio button",
  199. @"radiogroup" : @"radio group",
  200. @"scrollbar" : @"scroll bar",
  201. @"spinbutton" : @"spin button",
  202. @"switch" : @"switch",
  203. @"tab" : @"tab",
  204. @"tablist" : @"tab list",
  205. @"timer" : @"timer",
  206. @"toolbar" : @"tool bar",
  207. @"checked" : @"checked",
  208. @"unchecked" : @"not checked",
  209. @"busy" : @"busy",
  210. @"expanded" : @"expanded",
  211. @"collapsed" : @"collapsed",
  212. @"mixed" : @"mixed",
  213. };
  214. }
  215. });
  216. if ((self.accessibilityTraits & SwitchAccessibilityTrait) == SwitchAccessibilityTrait) {
  217. for (NSString *state in self.accessibilityState) {
  218. id val = self.accessibilityState[state];
  219. if (!val) {
  220. continue;
  221. }
  222. if ([state isEqualToString:@"checked"] && [val isKindOfClass:[NSNumber class]]) {
  223. return [val boolValue] ? @"1" : @"0";
  224. }
  225. }
  226. }
  227. NSMutableArray *valueComponents = [NSMutableArray new];
  228. NSString *roleDescription = self.accessibilityRole ? rolesAndStatesDescription[self.accessibilityRole] : nil;
  229. if (roleDescription) {
  230. [valueComponents addObject:roleDescription];
  231. }
  232. for (NSString *state in self.accessibilityState) {
  233. id val = self.accessibilityState[state];
  234. if (!val) {
  235. continue;
  236. }
  237. if ([state isEqualToString:@"checked"]) {
  238. if ([val isKindOfClass:[NSNumber class]]) {
  239. [valueComponents addObject:rolesAndStatesDescription[[val boolValue] ? @"checked" : @"unchecked"]];
  240. } else if ([val isKindOfClass:[NSString class]] && [val isEqualToString:@"mixed"]) {
  241. [valueComponents addObject:rolesAndStatesDescription[@"mixed"]];
  242. }
  243. }
  244. if ([state isEqualToString:@"expanded"] && [val isKindOfClass:[NSNumber class]]) {
  245. [valueComponents addObject:rolesAndStatesDescription[[val boolValue] ? @"expanded" : @"collapsed"]];
  246. }
  247. if ([state isEqualToString:@"busy"] && [val isKindOfClass:[NSNumber class]] && [val boolValue]) {
  248. [valueComponents addObject:rolesAndStatesDescription[@"busy"]];
  249. }
  250. }
  251. // handle accessibilityValue
  252. if (self.accessibilityValueInternal) {
  253. id min = self.accessibilityValueInternal[@"min"];
  254. id now = self.accessibilityValueInternal[@"now"];
  255. id max = self.accessibilityValueInternal[@"max"];
  256. id text = self.accessibilityValueInternal[@"text"];
  257. if (text && [text isKindOfClass:[NSString class]]) {
  258. [valueComponents addObject:text];
  259. } else if (
  260. [min isKindOfClass:[NSNumber class]] && [now isKindOfClass:[NSNumber class]] &&
  261. [max isKindOfClass:[NSNumber class]] && ([min intValue] < [max intValue]) &&
  262. ([min intValue] <= [now intValue] && [now intValue] <= [max intValue])) {
  263. int val = ([now intValue] * 100) / ([max intValue] - [min intValue]);
  264. [valueComponents addObject:[NSString stringWithFormat:@"%d percent", val]];
  265. }
  266. }
  267. if (valueComponents.count > 0) {
  268. return [valueComponents componentsJoinedByString:@", "];
  269. }
  270. return nil;
  271. }
  272. - (void)setPointerEvents:(RCTPointerEvents)pointerEvents
  273. {
  274. _pointerEvents = pointerEvents;
  275. self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone);
  276. if (pointerEvents == RCTPointerEventsBoxNone) {
  277. self.accessibilityViewIsModal = NO;
  278. }
  279. }
  280. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
  281. {
  282. BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
  283. if (!canReceiveTouchEvents) {
  284. return nil;
  285. }
  286. // `hitSubview` is the topmost subview which was hit. The hit point can
  287. // be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
  288. UIView *hitSubview = nil;
  289. BOOL isPointInside = [self pointInside:point withEvent:event];
  290. BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly);
  291. if (needsHitSubview && (![self clipsToBounds] || isPointInside)) {
  292. // Take z-index into account when calculating the touch target.
  293. NSArray<UIView *> *sortedSubviews = [self reactZIndexSortedSubviews];
  294. // The default behaviour of UIKit is that if a view does not contain a point,
  295. // then no subviews will be returned from hit testing, even if they contain
  296. // the hit point. By doing hit testing directly on the subviews, we bypass
  297. // the strict containment policy (i.e., UIKit guarantees that every ancestor
  298. // of the hit view will return YES from -pointInside:withEvent:). See:
  299. // - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
  300. for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) {
  301. CGPoint convertedPoint = [subview convertPoint:point fromView:self];
  302. hitSubview = [subview hitTest:convertedPoint withEvent:event];
  303. if (hitSubview != nil) {
  304. break;
  305. }
  306. }
  307. }
  308. UIView *hitView = (isPointInside ? self : nil);
  309. switch (_pointerEvents) {
  310. case RCTPointerEventsNone:
  311. return nil;
  312. case RCTPointerEventsUnspecified:
  313. return hitSubview ?: hitView;
  314. case RCTPointerEventsBoxOnly:
  315. return hitView;
  316. case RCTPointerEventsBoxNone:
  317. return hitSubview;
  318. default:
  319. RCTLogError(@"Invalid pointer-events specified %lld on %@", (long long)_pointerEvents, self);
  320. return hitSubview ?: hitView;
  321. }
  322. }
  323. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  324. {
  325. if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
  326. return [super pointInside:point withEvent:event];
  327. }
  328. CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
  329. return CGRectContainsPoint(hitFrame, point);
  330. }
  331. - (UIView *)reactAccessibilityElement
  332. {
  333. return self;
  334. }
  335. - (BOOL)isAccessibilityElement
  336. {
  337. if (self.reactAccessibilityElement == self) {
  338. return [super isAccessibilityElement];
  339. }
  340. return NO;
  341. }
  342. - (BOOL)performAccessibilityAction:(NSString *)name
  343. {
  344. if (_onAccessibilityAction && accessibilityActionsNameMap[name]) {
  345. _onAccessibilityAction(@{@"actionName" : name, @"actionTarget" : self.reactTag});
  346. return YES;
  347. }
  348. return NO;
  349. }
  350. - (BOOL)accessibilityActivate
  351. {
  352. if ([self performAccessibilityAction:@"activate"]) {
  353. return YES;
  354. } else if (_onAccessibilityTap) {
  355. _onAccessibilityTap(nil);
  356. return YES;
  357. } else {
  358. return NO;
  359. }
  360. }
  361. - (BOOL)accessibilityPerformMagicTap
  362. {
  363. if ([self performAccessibilityAction:@"magicTap"]) {
  364. return YES;
  365. } else if (_onMagicTap) {
  366. _onMagicTap(nil);
  367. return YES;
  368. } else {
  369. return NO;
  370. }
  371. }
  372. - (BOOL)accessibilityPerformEscape
  373. {
  374. if ([self performAccessibilityAction:@"escape"]) {
  375. return YES;
  376. } else if (_onAccessibilityEscape) {
  377. _onAccessibilityEscape(nil);
  378. return YES;
  379. } else {
  380. return NO;
  381. }
  382. }
  383. - (void)accessibilityIncrement
  384. {
  385. [self performAccessibilityAction:@"increment"];
  386. }
  387. - (void)accessibilityDecrement
  388. {
  389. [self performAccessibilityAction:@"decrement"];
  390. }
  391. - (NSString *)description
  392. {
  393. NSString *superDescription = super.description;
  394. NSRange semicolonRange = [superDescription rangeOfString:@";"];
  395. NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@;", self.reactTag];
  396. return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
  397. }
  398. #pragma mark - Statics for dealing with layoutGuides
  399. + (void)autoAdjustInsetsForView:(UIView<RCTAutoInsetsProtocol> *)parentView
  400. withScrollView:(UIScrollView *)scrollView
  401. updateOffset:(BOOL)updateOffset
  402. {
  403. UIEdgeInsets baseInset = parentView.contentInset;
  404. CGFloat previousInsetTop = scrollView.contentInset.top;
  405. CGPoint contentOffset = scrollView.contentOffset;
  406. if (parentView.automaticallyAdjustContentInsets) {
  407. UIEdgeInsets autoInset = [self contentInsetsForView:parentView];
  408. baseInset.top += autoInset.top;
  409. baseInset.bottom += autoInset.bottom;
  410. baseInset.left += autoInset.left;
  411. baseInset.right += autoInset.right;
  412. }
  413. scrollView.contentInset = baseInset;
  414. scrollView.scrollIndicatorInsets = baseInset;
  415. if (updateOffset) {
  416. // If we're adjusting the top inset, then let's also adjust the contentOffset so that the view
  417. // elements above the top guide do not cover the content.
  418. // This is generally only needed when your views are initially laid out, for
  419. // manual changes to contentOffset, you can optionally disable this step
  420. CGFloat currentInsetTop = scrollView.contentInset.top;
  421. if (currentInsetTop != previousInsetTop) {
  422. contentOffset.y -= (currentInsetTop - previousInsetTop);
  423. scrollView.contentOffset = contentOffset;
  424. }
  425. }
  426. }
  427. + (UIEdgeInsets)contentInsetsForView:(UIView *)view
  428. {
  429. while (view) {
  430. UIViewController *controller = view.reactViewController;
  431. if (controller) {
  432. return (UIEdgeInsets){controller.topLayoutGuide.length, 0, controller.bottomLayoutGuide.length, 0};
  433. }
  434. view = view.superview;
  435. }
  436. return UIEdgeInsetsZero;
  437. }
  438. #pragma mark - View unmounting
  439. - (void)react_remountAllSubviews
  440. {
  441. if (_removeClippedSubviews) {
  442. for (UIView *view in self.reactSubviews) {
  443. if (view.superview != self) {
  444. [self addSubview:view];
  445. [view react_remountAllSubviews];
  446. }
  447. }
  448. } else {
  449. // If _removeClippedSubviews is false, we must already be showing all subviews
  450. [super react_remountAllSubviews];
  451. }
  452. }
  453. - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
  454. {
  455. // TODO (#5906496): for scrollviews (the primary use-case) we could
  456. // optimize this by only doing a range check along the scroll axis,
  457. // instead of comparing the whole frame
  458. if (!_removeClippedSubviews) {
  459. // Use default behavior if unmounting is disabled
  460. return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
  461. }
  462. if (self.reactSubviews.count == 0) {
  463. // Do nothing if we have no subviews
  464. return;
  465. }
  466. if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
  467. // Do nothing if layout hasn't happened yet
  468. return;
  469. }
  470. // Convert clipping rect to local coordinates
  471. clipRect = [clipView convertRect:clipRect toView:self];
  472. clipRect = CGRectIntersection(clipRect, self.bounds);
  473. clipView = self;
  474. // Mount / unmount views
  475. for (UIView *view in self.reactSubviews) {
  476. if (!CGSizeEqualToSize(CGRectIntersection(clipRect, view.frame).size, CGSizeZero)) {
  477. // View is at least partially visible, so remount it if unmounted
  478. [self addSubview:view];
  479. // Then test its subviews
  480. if (CGRectContainsRect(clipRect, view.frame)) {
  481. // View is fully visible, so remount all subviews
  482. [view react_remountAllSubviews];
  483. } else {
  484. // View is partially visible, so update clipped subviews
  485. [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
  486. }
  487. } else if (view.superview) {
  488. // View is completely outside the clipRect, so unmount it
  489. [view removeFromSuperview];
  490. }
  491. }
  492. }
  493. - (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews
  494. {
  495. if (!removeClippedSubviews && _removeClippedSubviews) {
  496. [self react_remountAllSubviews];
  497. }
  498. _removeClippedSubviews = removeClippedSubviews;
  499. }
  500. - (void)didUpdateReactSubviews
  501. {
  502. if (_removeClippedSubviews) {
  503. [self updateClippedSubviews];
  504. } else {
  505. [super didUpdateReactSubviews];
  506. }
  507. }
  508. - (void)updateClippedSubviews
  509. {
  510. // Find a suitable view to use for clipping
  511. UIView *clipView = [self react_findClipView];
  512. if (clipView) {
  513. [self react_updateClippedSubviewsWithClipRect:clipView.bounds relativeToView:clipView];
  514. }
  515. }
  516. - (void)layoutSubviews
  517. {
  518. // TODO (#5906496): this a nasty performance drain, but necessary
  519. // to prevent gaps appearing when the loading spinner disappears.
  520. // We might be able to fix this another way by triggering a call
  521. // to updateClippedSubviews manually after loading
  522. [super layoutSubviews];
  523. if (_removeClippedSubviews) {
  524. [self updateClippedSubviews];
  525. }
  526. }
  527. - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
  528. {
  529. [super traitCollectionDidChange:previousTraitCollection];
  530. #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
  531. if (@available(iOS 13.0, *)) {
  532. if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
  533. [self.layer setNeedsDisplay];
  534. }
  535. }
  536. #endif
  537. }
  538. #pragma mark - Borders
  539. - (UIColor *)backgroundColor
  540. {
  541. return _backgroundColor;
  542. }
  543. - (void)setBackgroundColor:(UIColor *)backgroundColor
  544. {
  545. if ([_backgroundColor isEqual:backgroundColor]) {
  546. return;
  547. }
  548. _backgroundColor = backgroundColor;
  549. [self.layer setNeedsDisplay];
  550. }
  551. static CGFloat RCTDefaultIfNegativeTo(CGFloat defaultValue, CGFloat x)
  552. {
  553. return x >= 0 ? x : defaultValue;
  554. };
  555. - (UIEdgeInsets)bordersAsInsets
  556. {
  557. const CGFloat borderWidth = MAX(0, _borderWidth);
  558. const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
  559. if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
  560. const CGFloat borderStartWidth = RCTDefaultIfNegativeTo(_borderLeftWidth, _borderStartWidth);
  561. const CGFloat borderEndWidth = RCTDefaultIfNegativeTo(_borderRightWidth, _borderEndWidth);
  562. const CGFloat directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth;
  563. const CGFloat directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth;
  564. return (UIEdgeInsets){
  565. RCTDefaultIfNegativeTo(borderWidth, _borderTopWidth),
  566. RCTDefaultIfNegativeTo(borderWidth, directionAwareBorderLeftWidth),
  567. RCTDefaultIfNegativeTo(borderWidth, _borderBottomWidth),
  568. RCTDefaultIfNegativeTo(borderWidth, directionAwareBorderRightWidth),
  569. };
  570. }
  571. const CGFloat directionAwareBorderLeftWidth = isRTL ? _borderEndWidth : _borderStartWidth;
  572. const CGFloat directionAwareBorderRightWidth = isRTL ? _borderStartWidth : _borderEndWidth;
  573. return (UIEdgeInsets){
  574. RCTDefaultIfNegativeTo(borderWidth, _borderTopWidth),
  575. RCTDefaultIfNegativeTo(borderWidth, RCTDefaultIfNegativeTo(_borderLeftWidth, directionAwareBorderLeftWidth)),
  576. RCTDefaultIfNegativeTo(borderWidth, _borderBottomWidth),
  577. RCTDefaultIfNegativeTo(borderWidth, RCTDefaultIfNegativeTo(_borderRightWidth, directionAwareBorderRightWidth)),
  578. };
  579. }
  580. - (RCTCornerRadii)cornerRadii
  581. {
  582. const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
  583. const CGFloat radius = MAX(0, _borderRadius);
  584. CGFloat topLeftRadius;
  585. CGFloat topRightRadius;
  586. CGFloat bottomLeftRadius;
  587. CGFloat bottomRightRadius;
  588. if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
  589. const CGFloat topStartRadius = RCTDefaultIfNegativeTo(_borderTopLeftRadius, _borderTopStartRadius);
  590. const CGFloat topEndRadius = RCTDefaultIfNegativeTo(_borderTopRightRadius, _borderTopEndRadius);
  591. const CGFloat bottomStartRadius = RCTDefaultIfNegativeTo(_borderBottomLeftRadius, _borderBottomStartRadius);
  592. const CGFloat bottomEndRadius = RCTDefaultIfNegativeTo(_borderBottomRightRadius, _borderBottomEndRadius);
  593. const CGFloat directionAwareTopLeftRadius = isRTL ? topEndRadius : topStartRadius;
  594. const CGFloat directionAwareTopRightRadius = isRTL ? topStartRadius : topEndRadius;
  595. const CGFloat directionAwareBottomLeftRadius = isRTL ? bottomEndRadius : bottomStartRadius;
  596. const CGFloat directionAwareBottomRightRadius = isRTL ? bottomStartRadius : bottomEndRadius;
  597. topLeftRadius = RCTDefaultIfNegativeTo(radius, directionAwareTopLeftRadius);
  598. topRightRadius = RCTDefaultIfNegativeTo(radius, directionAwareTopRightRadius);
  599. bottomLeftRadius = RCTDefaultIfNegativeTo(radius, directionAwareBottomLeftRadius);
  600. bottomRightRadius = RCTDefaultIfNegativeTo(radius, directionAwareBottomRightRadius);
  601. } else {
  602. const CGFloat directionAwareTopLeftRadius = isRTL ? _borderTopEndRadius : _borderTopStartRadius;
  603. const CGFloat directionAwareTopRightRadius = isRTL ? _borderTopStartRadius : _borderTopEndRadius;
  604. const CGFloat directionAwareBottomLeftRadius = isRTL ? _borderBottomEndRadius : _borderBottomStartRadius;
  605. const CGFloat directionAwareBottomRightRadius = isRTL ? _borderBottomStartRadius : _borderBottomEndRadius;
  606. topLeftRadius =
  607. RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderTopLeftRadius, directionAwareTopLeftRadius));
  608. topRightRadius =
  609. RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderTopRightRadius, directionAwareTopRightRadius));
  610. bottomLeftRadius =
  611. RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderBottomLeftRadius, directionAwareBottomLeftRadius));
  612. bottomRightRadius = RCTDefaultIfNegativeTo(
  613. radius, RCTDefaultIfNegativeTo(_borderBottomRightRadius, directionAwareBottomRightRadius));
  614. }
  615. // Get scale factors required to prevent radii from overlapping
  616. const CGSize size = self.bounds.size;
  617. const CGFloat topScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (topLeftRadius + topRightRadius)));
  618. const CGFloat bottomScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (bottomLeftRadius + bottomRightRadius)));
  619. const CGFloat rightScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topRightRadius + bottomRightRadius)));
  620. const CGFloat leftScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topLeftRadius + bottomLeftRadius)));
  621. // Return scaled radii
  622. return (RCTCornerRadii){
  623. topLeftRadius * MIN(topScaleFactor, leftScaleFactor),
  624. topRightRadius * MIN(topScaleFactor, rightScaleFactor),
  625. bottomLeftRadius * MIN(bottomScaleFactor, leftScaleFactor),
  626. bottomRightRadius * MIN(bottomScaleFactor, rightScaleFactor),
  627. };
  628. }
  629. - (RCTBorderColors)borderColors
  630. {
  631. const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
  632. if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
  633. const CGColorRef borderStartColor = _borderStartColor ?: _borderLeftColor;
  634. const CGColorRef borderEndColor = _borderEndColor ?: _borderRightColor;
  635. const CGColorRef directionAwareBorderLeftColor = isRTL ? borderEndColor : borderStartColor;
  636. const CGColorRef directionAwareBorderRightColor = isRTL ? borderStartColor : borderEndColor;
  637. return (RCTBorderColors){
  638. _borderTopColor ?: _borderColor,
  639. directionAwareBorderLeftColor ?: _borderColor,
  640. _borderBottomColor ?: _borderColor,
  641. directionAwareBorderRightColor ?: _borderColor,
  642. };
  643. }
  644. const CGColorRef directionAwareBorderLeftColor = isRTL ? _borderEndColor : _borderStartColor;
  645. const CGColorRef directionAwareBorderRightColor = isRTL ? _borderStartColor : _borderEndColor;
  646. return (RCTBorderColors){
  647. _borderTopColor ?: _borderColor,
  648. directionAwareBorderLeftColor ?: _borderLeftColor ?: _borderColor,
  649. _borderBottomColor ?: _borderColor,
  650. directionAwareBorderRightColor ?: _borderRightColor ?: _borderColor,
  651. };
  652. }
  653. - (void)reactSetFrame:(CGRect)frame
  654. {
  655. // If frame is zero, or below the threshold where the border radii can
  656. // be rendered as a stretchable image, we'll need to re-render.
  657. // TODO: detect up-front if re-rendering is necessary
  658. CGSize oldSize = self.bounds.size;
  659. [super reactSetFrame:frame];
  660. if (!CGSizeEqualToSize(self.bounds.size, oldSize)) {
  661. [self.layer setNeedsDisplay];
  662. }
  663. }
  664. - (void)displayLayer:(CALayer *)layer
  665. {
  666. if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
  667. return;
  668. }
  669. RCTUpdateShadowPathForView(self);
  670. const RCTCornerRadii cornerRadii = [self cornerRadii];
  671. const UIEdgeInsets borderInsets = [self bordersAsInsets];
  672. const RCTBorderColors borderColors = [self borderColors];
  673. BOOL useIOSBorderRendering = RCTCornerRadiiAreEqual(cornerRadii) && RCTBorderInsetsAreEqual(borderInsets) &&
  674. RCTBorderColorsAreEqual(borderColors) && _borderStyle == RCTBorderStyleSolid &&
  675. // iOS draws borders in front of the content whereas CSS draws them behind
  676. // the content. For this reason, only use iOS border drawing when clipping
  677. // or when the border is hidden.
  678. (borderInsets.top == 0 || (borderColors.top && CGColorGetAlpha(borderColors.top) == 0) || self.clipsToBounds);
  679. // iOS clips to the outside of the border, but CSS clips to the inside. To
  680. // solve this, we'll need to add a container view inside the main view to
  681. // correctly clip the subviews.
  682. CGColorRef backgroundColor;
  683. #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
  684. if (@available(iOS 13.0, *)) {
  685. backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor;
  686. } else {
  687. backgroundColor = _backgroundColor.CGColor;
  688. }
  689. #else
  690. backgroundColor = _backgroundColor.CGColor;
  691. #endif
  692. if (useIOSBorderRendering) {
  693. layer.cornerRadius = cornerRadii.topLeft;
  694. layer.borderColor = borderColors.left;
  695. layer.borderWidth = borderInsets.left;
  696. layer.backgroundColor = backgroundColor;
  697. layer.contents = nil;
  698. layer.needsDisplayOnBoundsChange = NO;
  699. layer.mask = nil;
  700. return;
  701. }
  702. UIImage *image = RCTGetBorderImage(
  703. _borderStyle, layer.bounds.size, cornerRadii, borderInsets, borderColors, backgroundColor, self.clipsToBounds);
  704. layer.backgroundColor = NULL;
  705. if (image == nil) {
  706. layer.contents = nil;
  707. layer.needsDisplayOnBoundsChange = NO;
  708. return;
  709. }
  710. CGRect contentsCenter = ({
  711. CGSize size = image.size;
  712. UIEdgeInsets insets = image.capInsets;
  713. CGRectMake(
  714. insets.left / size.width, insets.top / size.height, (CGFloat)1.0 / size.width, (CGFloat)1.0 / size.height);
  715. });
  716. layer.contents = (id)image.CGImage;
  717. layer.contentsScale = image.scale;
  718. layer.needsDisplayOnBoundsChange = YES;
  719. layer.magnificationFilter = kCAFilterNearest;
  720. const BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
  721. if (isResizable) {
  722. layer.contentsCenter = contentsCenter;
  723. } else {
  724. layer.contentsCenter = CGRectMake(0.0, 0.0, 1.0, 1.0);
  725. }
  726. [self updateClippingForLayer:layer];
  727. }
  728. static BOOL RCTLayerHasShadow(CALayer *layer)
  729. {
  730. return layer.shadowOpacity * CGColorGetAlpha(layer.shadowColor) > 0;
  731. }
  732. static void RCTUpdateShadowPathForView(RCTView *view)
  733. {
  734. if (RCTLayerHasShadow(view.layer)) {
  735. if (CGColorGetAlpha(view.backgroundColor.CGColor) > 0.999) {
  736. // If view has a solid background color, calculate shadow path from border
  737. const RCTCornerRadii cornerRadii = [view cornerRadii];
  738. const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero);
  739. CGPathRef shadowPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL);
  740. view.layer.shadowPath = shadowPath;
  741. CGPathRelease(shadowPath);
  742. } else {
  743. // Can't accurately calculate box shadow, so fall back to pixel-based shadow
  744. view.layer.shadowPath = nil;
  745. RCTLogAdvice(
  746. @"View #%@ of type %@ has a shadow set but cannot calculate "
  747. "shadow efficiently. Consider setting a background color to "
  748. "fix this, or apply the shadow to a more specific component.",
  749. view.reactTag,
  750. [view class]);
  751. }
  752. }
  753. }
  754. - (void)updateClippingForLayer:(CALayer *)layer
  755. {
  756. CALayer *mask = nil;
  757. CGFloat cornerRadius = 0;
  758. if (self.clipsToBounds) {
  759. const RCTCornerRadii cornerRadii = [self cornerRadii];
  760. if (RCTCornerRadiiAreEqual(cornerRadii)) {
  761. cornerRadius = cornerRadii.topLeft;
  762. } else {
  763. CAShapeLayer *shapeLayer = [CAShapeLayer layer];
  764. CGPathRef path =
  765. RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL);
  766. shapeLayer.path = path;
  767. CGPathRelease(path);
  768. mask = shapeLayer;
  769. }
  770. }
  771. layer.cornerRadius = cornerRadius;
  772. layer.mask = mask;
  773. }
  774. #pragma mark Border Color
  775. #define setBorderColor(side) \
  776. -(void)setBorder##side##Color : (CGColorRef)color \
  777. { \
  778. if (CGColorEqualToColor(_border##side##Color, color)) { \
  779. return; \
  780. } \
  781. CGColorRelease(_border##side##Color); \
  782. _border##side##Color = CGColorRetain(color); \
  783. [self.layer setNeedsDisplay]; \
  784. }
  785. setBorderColor() setBorderColor(Top) setBorderColor(Right) setBorderColor(Bottom) setBorderColor(Left)
  786. setBorderColor(Start) setBorderColor(End)
  787. #pragma mark - Border Width
  788. #define setBorderWidth(side) \
  789. -(void)setBorder##side##Width : (CGFloat)width \
  790. { \
  791. if (_border##side##Width == width) { \
  792. return; \
  793. } \
  794. _border##side##Width = width; \
  795. [self.layer setNeedsDisplay]; \
  796. }
  797. setBorderWidth() setBorderWidth(Top) setBorderWidth(Right) setBorderWidth(Bottom) setBorderWidth(Left)
  798. setBorderWidth(Start) setBorderWidth(End)
  799. #pragma mark - Border Radius
  800. #define setBorderRadius(side) \
  801. -(void)setBorder##side##Radius : (CGFloat)radius \
  802. { \
  803. if (_border##side##Radius == radius) { \
  804. return; \
  805. } \
  806. _border##side##Radius = radius; \
  807. [self.layer setNeedsDisplay]; \
  808. }
  809. setBorderRadius() setBorderRadius(TopLeft) setBorderRadius(TopRight) setBorderRadius(TopStart)
  810. setBorderRadius(TopEnd) setBorderRadius(BottomLeft) setBorderRadius(BottomRight)
  811. setBorderRadius(BottomStart) setBorderRadius(BottomEnd)
  812. #pragma mark - Border Style
  813. #define setBorderStyle(side) \
  814. -(void)setBorder##side##Style : (RCTBorderStyle)style \
  815. { \
  816. if (_border##side##Style == style) { \
  817. return; \
  818. } \
  819. _border##side##Style = style; \
  820. [self.layer setNeedsDisplay]; \
  821. }
  822. setBorderStyle()
  823. - (void)dealloc
  824. {
  825. CGColorRelease(_borderColor);
  826. CGColorRelease(_borderTopColor);
  827. CGColorRelease(_borderRightColor);
  828. CGColorRelease(_borderBottomColor);
  829. CGColorRelease(_borderLeftColor);
  830. CGColorRelease(_borderStartColor);
  831. CGColorRelease(_borderEndColor);
  832. }
  833. @end