Advertisement
devil2010

Untitled

Jul 26th, 2022
2,054
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 31.93 KB | None | 0 0
  1. //
  2. // RNIContextMenuView.swift
  3. // nativeUIModulesTest
  4. //
  5. // Created by Dominic Go on 7/14/20.
  6. //
  7.  
  8. import UIKit;
  9.  
  10.  
  11. @available(iOS 13, *)
  12. class RNIContextMenuView: UIView {
  13.  
  14. enum NativeIDKey: String {
  15. case contextMenuPreview;
  16. case contextMenuAuxiliaryPreview;
  17. };
  18.  
  19. private enum AnchorPosition {
  20. case top;
  21. case bottom;
  22. };
  23.  
  24. // MARK: - Properties
  25. // ------------------
  26.  
  27. weak var bridge: RCTBridge!;
  28.  
  29. /// Keep track on whether or not the context menu is currently visible.
  30. var isContextMenuVisible = false;
  31.  
  32. /// Is set to `true` when the menu is open and an item is pressed and is immediately set back
  33. /// to `false` once the menu close animation finishes.
  34. var didPressMenuItem = false;
  35.  
  36. var contextMenuInteraction: UIContextMenuInteraction?;
  37.  
  38. /// contains the view to show in the preview
  39. var previewWrapper: RNIWrapperView?;
  40. var previewController: RNIContextMenuPreviewController?;
  41.  
  42. weak var contextMenuViewController: RNIContextMenuViewController?;
  43.  
  44. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  45. /// Holds the view to be shown in the auxiliary preview
  46. weak var previewAuxiliaryViewWrapper: RNIWrapperView?;
  47.  
  48. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  49. private var previewAuxiliaryViewPlacement: AnchorPosition?;
  50.  
  51. private var didTriggerCleanup = false;
  52.  
  53. /// Whether or not the current view was successfully added as child VC
  54. private var didAttachToParentVC = false;
  55.  
  56. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  57. private var shouldEnableAuxPreview = false;
  58.  
  59. // MARK: - RN Exported Event Props
  60. // -------------------------------
  61.  
  62. @objc var onMenuWillShow : RCTBubblingEventBlock?;
  63. @objc var onMenuWillHide : RCTBubblingEventBlock?;
  64. @objc var onMenuWillCancel: RCTBubblingEventBlock?;
  65.  
  66. @objc var onMenuDidShow : RCTBubblingEventBlock?;
  67. @objc var onMenuDidHide : RCTBubblingEventBlock?;
  68. @objc var onMenuDidCancel: RCTBubblingEventBlock?;
  69.  
  70. @objc var onPressMenuItem : RCTBubblingEventBlock?;
  71. @objc var onPressMenuPreview: RCTBubblingEventBlock?;
  72.  
  73. @objc var onMenuWillCreate: RCTBubblingEventBlock?;
  74.  
  75. // MARK: - RN Exported Props
  76. // -------------------------
  77.  
  78. private var _menuConfig: RNIMenuItem?;
  79. @objc var menuConfig: NSDictionary? {
  80. didSet {
  81. guard
  82. let menuConfigDict = self.menuConfig, menuConfigDict.count > 0,
  83. let menuConfig = RNIMenuItem(dictionary: menuConfigDict)
  84. else {
  85. self._menuConfig = nil;
  86. return;
  87. };
  88.  
  89. menuConfig.shouldUseDiscoverabilityTitleAsFallbackValueForSubtitle =
  90. self.shouldUseDiscoverabilityTitleAsFallbackValueForSubtitle;
  91.  
  92. #if DEBUG
  93. print("menuConfig didSet"
  94. + " - RNIMenuItem init"
  95. + " - menuConfig count: \(menuConfigDict.count)"
  96. );
  97. #endif
  98.  
  99. self._menuConfig = menuConfig;
  100.  
  101. if #available(iOS 14.0, *),
  102. self.isContextMenuVisible,
  103. let interaction: UIContextMenuInteraction = self.contextMenuInteraction {
  104.  
  105. #if DEBUG
  106. print("menuConfig didSet"
  107. + " - Updating visible menu"
  108. + " - menuItems: \(menuConfigDict["menuItems"] ?? "N/A")"
  109. );
  110. #endif
  111.  
  112. // context menu is open, update the menu items
  113. interaction.updateVisibleMenu {(menu: UIMenu) in
  114. return menuConfig.createMenu {(dict, action) in
  115. // menu item has been pressed...
  116. self.didPressMenuItem = true;
  117. self.onPressMenuItem?(dict);
  118. };
  119. };
  120. };
  121. }
  122. };
  123.  
  124. private var _previewConfig = PreviewConfig();
  125. @objc var previewConfig: NSDictionary? {
  126. didSet {
  127. guard let dictionary = self.previewConfig
  128. else { return };
  129.  
  130. let previewConfig = PreviewConfig(dictionary: dictionary);
  131. self._previewConfig = previewConfig;
  132.  
  133. // update the vc's previewConfig
  134. if let previewController = self.previewController {
  135. previewController.previewConfig = previewConfig;
  136. };
  137. }
  138. };
  139.  
  140. private var _auxiliaryPreviewConfig: RNIContextMenuAuxiliaryPreviewConfig?;
  141. @objc var auxiliaryPreviewConfig: NSDictionary? {
  142. didSet {
  143. guard
  144. let configDict = self.auxiliaryPreviewConfig,
  145. configDict.count > 0
  146. else {
  147. self._auxiliaryPreviewConfig = nil;
  148. return;
  149. };
  150.  
  151. let config = RNIContextMenuAuxiliaryPreviewConfig(dictionary: configDict);
  152. self._auxiliaryPreviewConfig = config;
  153. }
  154. };
  155.  
  156. @objc var shouldUseDiscoverabilityTitleAsFallbackValueForSubtitle = true;
  157.  
  158. @objc var isContextMenuEnabled = true;
  159.  
  160. // MARK: - Computed Properties
  161.  
  162. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  163. /// Gets the `_UIContextMenuContainerView` that's holding the context menu controls.
  164. /// **Note**: This `UIView` instance only exists whenever there's a context menu interaction.
  165. ///
  166. /// Contains the ff. subviews:
  167. /// * `UIVisualEffectView` - BG blur view
  168. /// * `UIView` - Holds `_UIMorphingPlatterView` and `_UIContextMenu`
  169. ///
  170. /// Debug Description
  171. /// ```
  172. /// <_UIContextMenuContainerView:
  173. /// frame = (0 0; 375 667); autoresize = W+H;
  174. /// gestureRecognizers = <NSArray>;
  175. /// layer = <CALayer>;
  176. /// >
  177. /// ```
  178. ///
  179. /// View Hierarchy (as of iOS `15.2`)
  180. /// ```
  181. /// <_UIContextMenuContainerView>
  182. /// // Blur background view
  183. /// <UIVisualEffectView>
  184. /// <_UIVisualEffectBackdropView/>
  185. /// </UIVisualEffectView>
  186. ///
  187. /// // Contains the context menu preview and list items, as well as
  188. /// // the aux. preview.
  189. /// <UIView>
  190. /// // Contains the context menu preview
  191. /// <_UIMorphingPlatterView>
  192. /// <_UIPlatterSoftShadowView/>
  193. /// <_UIPlatterClippingView/>
  194. /// <_UIPlatterClippingView/>
  195. /// </_UIMorphingPlatterView>
  196. ///
  197. /// // Contains the context menu list
  198. /// <_UIContextMenuView>
  199. /// <_UIContextMenuListView>
  200. /// <UIView>
  201. /// <UIView>
  202. ///
  203. /// // Blur backdrop for menu items list
  204. /// <UIVisualEffectView>
  205. /// <_UIVisualEffectBackdropView/>
  206. /// </UIVisualEffectView>
  207. ///
  208. /// // Contains the menu items
  209. /// <UICollectionView/>
  210. ///
  211. /// </UIView>
  212. /// </UIView>
  213. /// </_UIContextMenuListView>
  214. /// </_UIContextMenuView>
  215. ///
  216. /// // This is the aux. preview we inserted...
  217. /// <RCTView/>
  218. /// </UIView>
  219. /// </_UIContextMenuContainerView>
  220. /// ```
  221. var contextMenuContainerView: UIView? {
  222. self.window?.subviews.first {
  223. ($0.gestureRecognizers?.count ?? 0) > 0
  224. };
  225. };
  226.  
  227. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  228. /// Will return the ff. subviews:
  229. /// * `_UIMorphingPlatterView` - Contains the context menu preview
  230. /// * `_UIContextMenu` - Holds the context menu items
  231. ///
  232. var contextMenuContentContainer: UIView? {
  233. self.contextMenuContainerView?.subviews.first {
  234. !($0 is UIVisualEffectView) && $0.subviews.count > 0
  235. };
  236. };
  237.  
  238. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  239. /// Holds the context menu preview
  240. var morphingPlatterView: UIView? {
  241. self.contextMenuContentContainer?.subviews.first {
  242. ($0.gestureRecognizers?.count ?? 0) == 1;
  243. };
  244. };
  245.  
  246. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  247. /// Holds the context menu items
  248. var contextMenuItemsView: UIView? {
  249. self.contextMenuContentContainer?.subviews.first {
  250. ($0.gestureRecognizers?.count ?? 0) > 1;
  251. };
  252. };
  253.  
  254. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  255. var isPreviewAuxiliaryViewAttached: Bool {
  256. self.previewAuxiliaryViewWrapper != nil;
  257. };
  258.  
  259. // MARK: - Init
  260. // ------------
  261.  
  262. init(bridge: RCTBridge) {
  263. super.init(frame: CGRect());
  264.  
  265. self.bridge = bridge;
  266.  
  267. // Add context menu interaction...
  268. self.setupAddContextMenuInteraction();
  269.  
  270. #if DEBUG
  271. // `RCTInvalidating` doesn't trigger in view instance, so use observer
  272. NotificationCenter.default.addObserver(self,
  273. selector: #selector(self.onRCTBridgeWillReloadNotification),
  274. name: NSNotification.Name(rawValue: "RCTBridgeWillReloadNotification"),
  275. object: nil
  276. );
  277. #endif
  278. };
  279.  
  280. required init?(coder: NSCoder) {
  281. fatalError("init(coder:) has not been implemented");
  282. };
  283.  
  284. // MARK: - RN Lifecycle
  285. // --------------------
  286.  
  287. override func reactSetFrame(_ frame: CGRect) {
  288. super.reactSetFrame(frame);
  289. };
  290.  
  291. override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
  292. super.insertSubview(subview, at: atIndex);
  293.  
  294. if let wrapperView = subview as? RNIWrapperView,
  295. let nativeID = subview.nativeID,
  296. let nativeIDKey = NativeIDKey(rawValue: nativeID) {
  297.  
  298. wrapperView.willChangeSuperview = true;
  299. wrapperView.autoCleanupOnJSUnmount = true;
  300.  
  301. switch nativeIDKey {
  302. case .contextMenuPreview:
  303. // if prev. exist, cleanup if needed.
  304. self.previewWrapper?.cleanup();
  305. self.previewWrapper = wrapperView;
  306.  
  307. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  308. case .contextMenuAuxiliaryPreview:
  309. // TODO: if prev. exist, cleanup if needed.
  310. // self.previewAuxiliaryViewWrapper?.cleanup();
  311. self.previewAuxiliaryViewWrapper = wrapperView;
  312. };
  313.  
  314. wrapperView.removeFromSuperview();
  315. wrapperView.willChangeSuperview = false;
  316. };
  317. };
  318.  
  319. #if DEBUG
  320. @objc func onRCTBridgeWillReloadNotification(_ notification: Notification){
  321. self.cleanup();
  322. };
  323. #endif
  324.  
  325. // MARK: - View Lifecycle
  326. // ----------------------
  327.  
  328. public override func didMoveToWindow() {
  329. if self.window == nil,
  330. !self.didAttachToParentVC {
  331.  
  332. // not using UINavigationController... manual cleanup
  333. self.cleanup();
  334.  
  335. } else if self.window != nil,
  336. !self.didAttachToParentVC {
  337.  
  338. // setup - might be using UINavigationController, attach as child vc
  339. self.attachToParentVC();
  340. };
  341. };
  342. };
  343.  
  344. // MARK: - View Module Commands
  345. // ----------------------------
  346.  
  347. @available(iOS 13, *)
  348. extension RNIContextMenuView {
  349. func dismissMenu(){
  350. self.contextMenuInteraction?.dismissMenu();
  351. };
  352. };
  353.  
  354. // MARK: - Private Functions
  355. // -------------------------
  356.  
  357. @available(iOS 13, *)
  358. fileprivate extension RNIContextMenuView {
  359.  
  360. /// Add a context menu interaction to view
  361. func setupAddContextMenuInteraction(){
  362. self.contextMenuInteraction = {
  363. let interaction = UIContextMenuInteraction(delegate: self);
  364. self.addInteraction(interaction);
  365.  
  366. return interaction;
  367. }();
  368. };
  369.  
  370. func cleanup(){
  371.  
  372. guard !self.didTriggerCleanup else { return };
  373. self.didTriggerCleanup = true;
  374.  
  375. self.contextMenuInteraction?.dismissMenu();
  376. self.contextMenuInteraction = nil;
  377.  
  378. // remove preview from registry
  379. self.previewWrapper?.cleanup();
  380.  
  381. // remove this view from registry
  382. RNIUtilities.recursivelyRemoveFromViewRegistry(
  383. bridge : self.bridge,
  384. reactView: self
  385. );
  386.  
  387. #if DEBUG
  388. NotificationCenter.default.removeObserver(self);
  389. #endif
  390. };
  391.  
  392. /// create `UIMenu` based on `menuConfig` prop
  393. func createMenu(_ suggestedAction: [UIMenuElement]) -> UIMenu? {
  394. guard let menuConfig = self._menuConfig else {
  395. #if DEBUG
  396. print("RNIContextMenuView, createMenu"
  397. + " - guard check failed, menuConfig: nil"
  398. );
  399. #endif
  400. return nil;
  401. };
  402.  
  403. return menuConfig.createMenu { (dict, action) in
  404. // menu item has been pressed...
  405. self.didPressMenuItem = true;
  406. self.onPressMenuItem?(dict);
  407. };
  408. };
  409.  
  410. /// create custom menu preview based on `previewConfig` and `reactPreviewView`
  411. func createMenuPreview() -> UIViewController? {
  412. // alias to variable
  413. let previewConfig = self._previewConfig;
  414.  
  415. /// don't make preview if `previewType` is set to default.
  416. guard previewConfig.previewType != .DEFAULT
  417. else { return nil };
  418.  
  419. // vc that holds the view to show in the preview
  420. let vc = RNIContextMenuPreviewController();
  421. vc.previewWrapper = self.previewWrapper;
  422. vc.previewConfig = previewConfig;
  423. vc.view.isUserInteractionEnabled = true;
  424.  
  425. self.previewController = vc;
  426. return vc;
  427. };
  428.  
  429. /// configure target preview based on `previewConfig`
  430. func makeTargetedPreview() -> UITargetedPreview {
  431. // alias to variable
  432. let previewConfig = self._previewConfig;
  433.  
  434. // create preview parameters based on `previewConfig`
  435. let parameters: UIPreviewParameters = {
  436. let param = UIPreviewParameters();
  437.  
  438. // set preview bg color
  439. param.backgroundColor = previewConfig.backgroundColor;
  440.  
  441. // set the preview border shape
  442. if let borderRadius = previewConfig.borderRadius {
  443. let previewShape = UIBezierPath(
  444. // get width/height from custom preview view
  445. roundedRect: CGRect(
  446. origin: CGPoint(x: 0, y: 0),
  447. size : self.frame.size
  448. ),
  449. // set the preview corner radius
  450. cornerRadius: borderRadius
  451. );
  452.  
  453. // set preview border shape
  454. param.visiblePath = previewShape;
  455.  
  456. // set preview border shadow
  457. if #available(iOS 14, *){
  458. param.shadowPath = previewShape;
  459. };
  460. };
  461.  
  462. return param;
  463. }();
  464.  
  465. if let targetNode = previewConfig.targetViewNode,
  466. let targetView = self.bridge.uiManager.view(forReactTag: targetNode) {
  467.  
  468. // A - Targeted preview provided....
  469. return UITargetedPreview(
  470. view: targetView,
  471. parameters: parameters
  472. );
  473.  
  474. } else {
  475. // B - No targeted preview provided....
  476. return UITargetedPreview(
  477. view: self,
  478. parameters: parameters
  479. );
  480. };
  481. };
  482.  
  483. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  484. // TODO: Make it appear faster
  485. func attachContextMenuAuxiliaryPreviewIfAny(_ animator: UIContextMenuInteractionAnimating!){
  486. print("shouldEnableAuxPreview \(shouldEnableAuxPreview)")
  487. guard self.shouldEnableAuxPreview,
  488. let previewAuxiliaryViewWrapper = self.previewAuxiliaryViewWrapper,
  489. let previewAuxiliaryView = previewAuxiliaryViewWrapper.reactContent,
  490.  
  491. let contextMenuContentContainer = self.contextMenuContentContainer,
  492. let contextMenuContainerView = self.contextMenuContainerView,
  493. let morphingPlatterView = self.morphingPlatterView
  494. else { return };
  495.  
  496. // MARK: Prep - Set Constants
  497. // --------------------------
  498.  
  499. let auxConfig = self._auxiliaryPreviewConfig
  500. ?? RNIContextMenuAuxiliaryPreviewConfig(dictionary: [:]);
  501.  
  502. let auxiliaryViewHeight: CGFloat = {
  503. // A - Use height from config
  504. if let height = auxConfig.height {
  505. return height;
  506. };
  507.  
  508. // B - Infer aux preview height from view
  509. return previewAuxiliaryView.frame.height;
  510. }();
  511.  
  512. let auxiliaryViewWidth: CGFloat = {
  513. // amount to subtract from width - fix for layout bug
  514. // if you use the full width, it triggers a bug w/ autolayout where the
  515. // aux. preview snaps to the top of the screen...
  516. let adj = 0.5;
  517.  
  518. switch auxConfig.alignmentHorizontal {
  519. // A - Set aux preview width to window width
  520. case .stretchScreen:
  521. return contextMenuContainerView.frame.width - adj;
  522.  
  523. // B - Set aux preview width to preview width
  524. case .stretchPreview:
  525. return morphingPlatterView.frame.width - adj;
  526.  
  527. // C - Infer aux preview width from view...
  528. default:
  529. return previewAuxiliaryView.frame.width;
  530. };
  531. }();
  532.  
  533. let marginInner = auxConfig.marginPreview;
  534. let marginOuter = auxConfig.marginAuxiliaryPreview;
  535.  
  536. let previewAuxiliaryViewSize = CGSize(
  537. width : auxiliaryViewWidth,
  538. height: auxiliaryViewHeight
  539. );
  540.  
  541. // MARK: Prep - Determine Size and Position
  542. // ----------------------------------------
  543.  
  544. /// * Determine the size and position of the context menu preview.
  545. /// * Determine where to place the aux. preview in relation to the context menu preview.
  546.  
  547. /// Based on the current "menu config", does it have menu items?
  548. let menuConfigHasMenuItems: Bool = {
  549. guard let menuItems = self._menuConfig?.menuItems
  550. else { return false };
  551.  
  552. return menuItems.count > 0;
  553. }();
  554.  
  555. /// if the context menu has "menu items", where is it located in relation to the "context menu preview"?
  556. let menuItemsPlacement: AnchorPosition? = {
  557. guard menuConfigHasMenuItems,
  558. let contextMenuItemsView = self.contextMenuItemsView
  559. else { return nil };
  560.  
  561. let previewFrame = morphingPlatterView.frame;
  562. let menuItemsFrame = contextMenuItemsView.frame;
  563.  
  564. return (menuItemsFrame.midY < previewFrame.midY) ? .bottom : .top;
  565. }();
  566.  
  567. /// in which half does the "context menu preview" fall into?
  568. let morphingPlatterViewPlacement: AnchorPosition = {
  569. let previewFrame = morphingPlatterView.frame;
  570. let screenBounds = UIScreen.main.bounds;
  571.  
  572. return (previewFrame.midY < screenBounds.midY) ? .top : .bottom;
  573. }();
  574.  
  575. /// whether to attach the `auxiliaryView` on the top or bottom of the context menu
  576. let shouldAttachToTop: Bool = {
  577. switch auxConfig.anchorPosition {
  578. case .top : return true;
  579. case .bottom: return false;
  580.  
  581. case .automatic: break;
  582. };
  583.  
  584. switch menuItemsPlacement {
  585. case .top : return true;
  586. case .bottom: return false;
  587.  
  588. default:
  589. // the context menu does not have menu items, determine anchor position
  590. // of auxiliary view via the position of the preview in the screen
  591. return morphingPlatterViewPlacement == .bottom;
  592. };
  593. }();
  594.  
  595. // temp. save aux. preview position for later...
  596. self.previewAuxiliaryViewPlacement = morphingPlatterViewPlacement;
  597.  
  598. // MARK: Prep - Compute Offsets
  599. // ----------------------------
  600.  
  601. /// distance of aux preview from anchor
  602. let offset: CGFloat = {
  603. let safeAreaInsets = UIApplication.shared.windows.first?.safeAreaInsets;
  604.  
  605. let previewFrame = morphingPlatterView.frame;
  606. let screenHeight = UIScreen.main.bounds.height;
  607.  
  608. let marginBase = marginInner + marginOuter;
  609.  
  610. switch morphingPlatterViewPlacement {
  611. case .top:
  612. let topInsets = safeAreaInsets?.top ?? 0;
  613. let margin = marginBase + topInsets;
  614.  
  615. let minEdgeY = auxiliaryViewHeight + topInsets;
  616. let distanceToEdge = previewFrame.minY;
  617.  
  618. return (previewFrame.minY <= minEdgeY)
  619. ? max((auxiliaryViewHeight - distanceToEdge + margin), 0)
  620. : 0;
  621.  
  622. case .bottom:
  623. let bottomInsets = safeAreaInsets?.bottom ?? 0;
  624. let margin = marginBase + bottomInsets;
  625.  
  626. let tolerance = auxiliaryViewHeight + (safeAreaInsets?.bottom ?? 0);
  627. let maxEdgeY = screenHeight - tolerance;
  628.  
  629. let distanceToEdge = screenHeight - previewFrame.maxY;
  630. return (previewFrame.maxY > maxEdgeY)
  631. ? -(auxiliaryViewHeight - distanceToEdge + margin)
  632. : 0;
  633. };
  634. }();
  635.  
  636. // MARK: Set Layout
  637. // ----------------
  638.  
  639. // TODO: Remove?
  640. /// detach aux. preview
  641. previewAuxiliaryViewWrapper.removeFromSuperview();
  642. previewAuxiliaryView.removeFromSuperview();
  643.  
  644. // Bugfix: Stop bubbling touch events from propagating to parent
  645. previewAuxiliaryView.addGestureRecognizer(
  646. UITapGestureRecognizer(target: nil, action: nil)
  647. );
  648.  
  649. /// manually set size of aux. preview
  650. previewAuxiliaryViewWrapper
  651. .notifyForBoundsChange(size: previewAuxiliaryViewSize);
  652.  
  653. /// enable auto layout
  654. previewAuxiliaryView.translatesAutoresizingMaskIntoConstraints = false;
  655.  
  656. /// attach `auxiliaryView` to context menu preview
  657. contextMenuContentContainer.addSubview(previewAuxiliaryView);
  658.  
  659. // set layout constraints based on config
  660. NSLayoutConstraint.activate({
  661.  
  662. // set initial constraints
  663. var constraints: Array<NSLayoutConstraint> = [
  664. // set aux preview height
  665. previewAuxiliaryView.heightAnchor
  666. .constraint(equalToConstant: auxiliaryViewHeight),
  667. ];
  668.  
  669. // set vertical alignment constraint - i.e. either...
  670. constraints.append(shouldAttachToTop
  671. // A - pin to top or...
  672. ? previewAuxiliaryView.bottomAnchor
  673. .constraint(equalTo: morphingPlatterView.topAnchor, constant: -marginInner)
  674.  
  675. // B - pin to bottom.
  676. : previewAuxiliaryView.topAnchor
  677. .constraint(equalTo: morphingPlatterView.bottomAnchor, constant: marginInner)
  678. );
  679.  
  680. // set horizontal alignment constraints based on config...
  681. constraints += {
  682. switch auxConfig.alignmentHorizontal {
  683. // A - pin to left
  684. case .previewLeading: return [
  685. previewAuxiliaryView.leadingAnchor
  686. .constraint(equalTo: morphingPlatterView.leadingAnchor),
  687. ];
  688.  
  689. // B - pin to right
  690. case .previewTrailing: return [
  691. previewAuxiliaryView.rightAnchor.constraint(
  692. equalTo: morphingPlatterView.rightAnchor, constant: -auxiliaryViewWidth)
  693. ];
  694.  
  695. // C - pin to center
  696. case .previewCenter: return [
  697. previewAuxiliaryView.centerXAnchor
  698. .constraint(equalTo: morphingPlatterView.centerXAnchor),
  699. ];
  700.  
  701. // D - match preview size
  702. case .stretchPreview: return [
  703. previewAuxiliaryView.leadingAnchor
  704. .constraint(equalTo: morphingPlatterView.leadingAnchor),
  705.  
  706. previewAuxiliaryView.trailingAnchor
  707. .constraint(equalTo: morphingPlatterView.trailingAnchor),
  708. ];
  709.  
  710. // E - stretch to edges of screen
  711. case .stretchScreen: return [
  712. previewAuxiliaryView.leadingAnchor
  713. .constraint(equalTo: contextMenuContainerView.leadingAnchor),
  714.  
  715. previewAuxiliaryView.trailingAnchor
  716. .constraint(equalTo: contextMenuContainerView.trailingAnchor),
  717. ];
  718. };
  719. }();
  720.  
  721. return constraints;
  722. }());
  723.  
  724. // MARK: Show Aux. View
  725. // --------------------
  726.  
  727. // transition - start value
  728. previewAuxiliaryView.alpha = 0;
  729.  
  730. UIView.animate(withDuration: 0.3, animations: {
  731. // fade in transition
  732. previewAuxiliaryView.alpha = 1;
  733.  
  734. // offset from anchor
  735. contextMenuContentContainer.frame =
  736. contextMenuContentContainer.frame.offsetBy(dx: 0, dy: offset)
  737.  
  738. }, completion: {_ in
  739. // TODO: Add RN event
  740. });
  741. };
  742.  
  743. func viewByClassName(view: UIView, className: String) -> UIView? {
  744. let name = NSStringFromClass(type(of: view))
  745. if name == className {
  746. return view
  747. }else {
  748. for subview in view.subviews {
  749. if let view = viewByClassName(view: subview, className: className) {
  750. return view
  751. }
  752. }
  753. }
  754. return nil
  755. }
  756.  
  757. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  758. func detachContextMenuAuxiliaryPreviewIfAny(_ animator: UIContextMenuInteractionAnimating?){
  759.  
  760. guard self.shouldEnableAuxPreview,
  761. let animator = animator,
  762. let previewAuxiliaryView = self.previewAuxiliaryViewWrapper?.reactContent
  763. else { return };
  764.  
  765. /// Bug:
  766. /// * "Could not locate shadow view with tag #, this is probably caused by a temporary inconsistency
  767. /// between native views and shadow views."
  768. /// * Triggered when the menu is about to be hidden, iOS removes the context menu along with the
  769. /// `previewAuxiliaryViewContainer`
  770.  
  771. // Add exit transition
  772. animator.addAnimations {
  773. var transform = previewAuxiliaryView.transform;
  774.  
  775. // transition - fade out
  776. previewAuxiliaryView.alpha = 0;
  777.  
  778. // transition - zoom out
  779. transform = transform.scaledBy(x: 0.7, y: 0.7);
  780.  
  781. // transition - slide out
  782. switch self.previewAuxiliaryViewPlacement {
  783. case .top:
  784. transform = transform.translatedBy(x: 0, y: 50);
  785.  
  786. case .bottom:
  787. transform = transform.translatedBy(x: 0, y: -50);
  788.  
  789. default: break;
  790. };
  791.  
  792. // transition - apply transform
  793. previewAuxiliaryView.transform = transform;
  794. };
  795.  
  796. animator.addCompletion {
  797. previewAuxiliaryView.removeFromSuperview();
  798.  
  799. // clear value
  800. self.previewAuxiliaryViewPlacement = nil;
  801. };
  802. };
  803. };
  804.  
  805. // MARK: - UIContextMenuInteractionDelegate
  806. // ----------------------------------------
  807.  
  808. @available(iOS 13, *)
  809. extension RNIContextMenuView: UIContextMenuInteractionDelegate {
  810.  
  811. // create context menu
  812. func contextMenuInteraction(
  813. _ interaction: UIContextMenuInteraction,
  814. configurationForMenuAtLocation location: CGPoint
  815. ) -> UIContextMenuConfiguration? {
  816.  
  817. guard self.isContextMenuEnabled else { return nil };
  818.  
  819. self.onMenuWillCreate?([:]);
  820.  
  821. // Note: Xcode beta + running on device (iPhone XR + iOS 15.1) causes
  822. // crashes when the context menu is being created
  823. return UIContextMenuConfiguration(
  824. identifier : nil,
  825. previewProvider: self.createMenuPreview,
  826. actionProvider : self.createMenu
  827. );
  828. };
  829.  
  830. // context menu display begins
  831. func contextMenuInteraction(
  832. _ interaction: UIContextMenuInteraction,
  833. willDisplayMenuFor configuration: UIContextMenuConfiguration,
  834. animator: UIContextMenuInteractionAnimating?
  835. ) {
  836.  
  837. #if DEBUG
  838. print("RNIContextMenuView, UIContextMenuInteractionDelegate"
  839. + " - contextMenuInteraction: will show"
  840. );
  841. #endif
  842.  
  843. self.isContextMenuVisible = true;
  844. self.onMenuWillShow?([:]);
  845.  
  846. if let previewController = self.previewController{
  847. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  848. if let window = UIApplication.shared.delegate?.window! {
  849. if let view = self.viewByClassName(view: window, className: "_UIPlatterClippingView") {
  850. view.addSubview(previewController.view)
  851. }
  852. }
  853. }
  854. }
  855.  
  856. animator?.addCompletion {
  857. self.onMenuDidShow?([:]);
  858. //
  859. // if let previewController = self.previewController{
  860. // previewController.view.subviews.first?.alpha = 0
  861. // }
  862.  
  863. #if DEBUG
  864. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  865. // show context menu auxiliary preview
  866. self.attachContextMenuAuxiliaryPreviewIfAny(animator);
  867. #endif
  868.  
  869. };
  870. };
  871.  
  872. // context menu display ends
  873. func contextMenuInteraction(
  874. _ interaction: UIContextMenuInteraction,
  875. willEndFor configuration: UIContextMenuConfiguration,
  876. animator: UIContextMenuInteractionAnimating?
  877. ) {
  878.  
  879. #if DEBUG
  880. print("RNIContextMenuView, UIContextMenuInteractionDelegate"
  881. + " - contextMenuInteraction: will hide"
  882. );
  883. #endif
  884.  
  885. guard self.isContextMenuVisible else { return };
  886.  
  887. #if DEBUG
  888. // MARK: Experimental - "Auxiliary Context Menu Preview"-Related
  889. // hide preview auxiliary view
  890. self.detachContextMenuAuxiliaryPreviewIfAny(animator);
  891. #endif
  892.  
  893. self.onMenuWillHide?([:]);
  894.  
  895. if !self.didPressMenuItem {
  896. // nothing was selected...
  897. self.onMenuWillCancel?([:]);
  898. };
  899.  
  900. animator?.addCompletion {
  901. self.onMenuDidHide?([:]);
  902.  
  903. if !self.didPressMenuItem {
  904. // nothing was selected...
  905. self.onMenuDidCancel?([:]);
  906. };
  907.  
  908. // reset flag
  909. self.didPressMenuItem = false;
  910. };
  911.  
  912. // reset flag
  913. self.isContextMenuVisible = false;
  914. };
  915.  
  916. // context menu preview tapped
  917. func contextMenuInteraction(
  918. _ interaction: UIContextMenuInteraction,
  919. willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
  920. animator: UIContextMenuInteractionCommitAnimating
  921. ) {
  922.  
  923. #if DEBUG
  924. print("RNIContextMenuView, UIContextMenuInteractionDelegate"
  925. + " - contextMenuInteraction: preview tapped"
  926. );
  927. #endif
  928.  
  929. let preferredCommitStyle = self._previewConfig.preferredCommitStyle;
  930.  
  931. self.isContextMenuVisible = false;
  932. animator.preferredCommitStyle = preferredCommitStyle;
  933.  
  934. switch preferredCommitStyle {
  935. case .pop:
  936. self.onMenuWillHide?([:]);
  937.  
  938. animator.addCompletion {
  939. self.onPressMenuPreview?([:]);
  940. self.onMenuDidHide?([:]);
  941. };
  942.  
  943. case .dismiss: fallthrough;
  944. @unknown default:
  945. self.onMenuWillHide?([:]);
  946. self.onPressMenuPreview?([:]);
  947.  
  948. animator.addCompletion {
  949. self.onMenuDidHide?([:]);
  950. };
  951. };
  952. };
  953.  
  954. #if swift(>=5.7)
  955. func contextMenuInteraction(
  956. _ interaction: UIContextMenuInteraction,
  957. configuration: UIContextMenuConfiguration,
  958. highlightPreviewForItemWithIdentifier identifier: NSCopying
  959. ) -> UITargetedPreview? {
  960.  
  961. return self.makeTargetedPreview();
  962. };
  963. #else
  964. /// deprecated in iOS 16
  965. func contextMenuInteraction(
  966. _ : UIContextMenuInteraction,
  967. previewForHighlightingMenuWithConfiguration: UIContextMenuConfiguration
  968. ) -> UITargetedPreview? {
  969.  
  970. return self.makeTargetedPreview();
  971. };
  972. #endif
  973.  
  974.  
  975. #if swift(>=5.7)
  976. func contextMenuInteraction(
  977. _ interaction: UIContextMenuInteraction,
  978. configuration: UIContextMenuConfiguration,
  979. dismissalPreviewForItemWithIdentifier identifier: NSCopying
  980. ) -> UITargetedPreview? {
  981.  
  982. return self.makeTargetedPreview();
  983. };
  984. #else
  985. /// deprecated in iOS 16
  986. func contextMenuInteraction(
  987. _ interaction: UIContextMenuInteraction,
  988. previewForDismissingMenuWithConfiguration
  989. configuration: UIContextMenuConfiguration
  990. ) -> UITargetedPreview? {
  991.  
  992. return self.makeTargetedPreview();
  993. };
  994. #endif
  995. };
  996.  
  997. // MARK: - RNIContextMenu
  998. // ----------------------
  999.  
  1000. @available(iOS 13, *)
  1001. extension RNIContextMenuView: RNIContextMenu {
  1002.  
  1003. func notifyViewControllerDidPop(sender: RNIContextMenuViewController) {
  1004. // trigger cleanup
  1005. self.cleanup();
  1006. };
  1007.  
  1008. func attachToParentVC(){
  1009. guard !self.didAttachToParentVC,
  1010. // find the nearest parent view controller
  1011. let parentVC = RNIUtilities
  1012. .getParent(responder: self, type: UIViewController.self)
  1013. else { return };
  1014.  
  1015. self.didAttachToParentVC = true;
  1016.  
  1017. let childVC = RNIContextMenuViewController(contextMenuView: self);
  1018. childVC.parentVC = parentVC;
  1019.  
  1020. self.contextMenuViewController = childVC;
  1021.  
  1022. parentVC.addChild(childVC);
  1023. childVC.didMove(toParent: parentVC);
  1024. };
  1025.  
  1026. func detachFromParentVC(){
  1027. guard !self.didAttachToParentVC,
  1028. let childVC = self.contextMenuViewController
  1029. else { return };
  1030.  
  1031. childVC.willMove(toParent: nil);
  1032. childVC.removeFromParent();
  1033. };
  1034. };
  1035.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement