From d38b8b5bbc24714454eb056bd9cbf564e06898b8 Mon Sep 17 00:00:00 2001 From: Dominic Go <18517029+dominicstop@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:26:11 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=95=20Add:=20Re-Add=20iOS-Related=20So?= =?UTF-8?q?urce=20Files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Extensions+Init/CGSize+Init.swift | 21 + ios/Extensions+Init/UIBlurEffect+Init.swift | 132 ++ .../UIModalPresentationStyle+Init.swift | 81 ++ .../UISheetPresentationController+Init.swift | 67 + ios/Extensions/CAAnimation+Block.swift | 61 + ios/Extensions/CAAnimation+Helpers.swift | 17 + ios/Extensions/CGRect+Helpers.swift | 54 + ios/Extensions/Collection+Helpers.swift | 41 + ios/Extensions/Encodable+Helpers.swift | 31 + ios/Extensions/FloatingPoint+Clamping.swift | 31 + ios/Extensions/KeyWindow+Helpers.swift | 21 + ios/Extensions/RNIUtilities+Helpers.swift | 158 ++ .../UIGestureRecognizer+Helpers.swift | 23 + .../UIModalTransitionStyle+Helpers.swift | 34 + ios/Extensions/UIView+Helpers.swift | 60 + .../UIViewController+Swizzling.swift | 235 +++ ios/Extensions/UIWindow+Helpers.swift | 12 + ios/Helpers+Utilities/CGSize+Helpers.swift | 14 + ios/Helpers+Utilities/WeakElement.swift | 13 + .../RNIAnimator/RNIAnimator.swift | 243 ++++ .../RNIAnimator/RNIAnimatorSize.swift | 54 + .../RNIComputable/RNIComputableOffset.swift | 61 + .../RNIComputable/RNIComputableSize.swift | 148 ++ .../RNIComputable/RNIComputableSizeMode.swift | 60 + .../RNIComputable/RNIComputableValue.swift | 116 ++ .../RNIComputableValueMode.swift | 48 + .../RNIComputable/RNIViewMetadata.swift | 62 + .../RNIDictionaryRepresentable.swift | 12 + .../RNIDictionarySynthesizable+Default.swift | 140 ++ .../RNIDictionarySynthesizable.swift | 29 + .../RNIError/RNIBaseError+Helpers.swift | 120 ++ ios/React Native/RNIError/RNIBaseError.swift | 34 + ios/React Native/RNIError/RNIError.swift | 18 + .../RNIError/RNIErrorCodeDefaultable.swift | 70 + .../RNIError/RNIErrorCodeSynthesizable.swift | 24 + .../RNIError/RNIGenericErrorCode.swift | 15 + .../RNIIdentifiable+Default.swift | 37 + .../RNIIdentifiable/RNIIdentifiable.swift | 20 + .../RNIIdentifiable/RNIObjectIdentifier.swift | 54 + .../RNIModal/RNIModal+Helpers.swift | 122 ++ ios/React Native/RNIModal/RNIModal.swift | 125 ++ .../RNIModal/RNIModalCustomSheetDetent.swift | 111 ++ ios/React Native/RNIModal/RNIModalData.swift | 28 + ios/React Native/RNIModal/RNIModalError.swift | 41 + .../RNIModal/RNIModalEventData.swift | 64 + ios/React Native/RNIModal/RNIModalFlags.swift | 21 + .../RNIModal/RNIModalFocusState.swift | 142 ++ .../RNIModal/RNIModalManager.swift | 448 ++++++ .../RNIModalPresentationNotifiable.swift | 19 + .../RNIModal/RNIModalPresentationState.swift | 303 ++++ .../RNIModalPresentationTrigger.swift | 16 + .../RNIModal/RNIModalUtilities.swift | 121 ++ .../RNIModal/RNIModalWindowMap.swift | 140 ++ .../RNIPresentedViewControllerCache.swift | 72 + .../RNIModalView/RNIModalView.swift | 1285 +++++++++++++++++ .../RNIModalView/RNIModalViewController.swift | 369 +++++ .../RNIModalView/RNIModalViewError.swift | 35 + .../RNIModalView/RNIModalViewManager.m | 83 ++ .../RNIModalView/RNIModalViewManager.swift | 44 + .../RNIModalViewControllerWrapper.swift | 132 ++ ...NIModalViewControllerWrapperRegistry.swift | 39 + .../RNIModalViewModule/RNIModalViewModule.m | 43 + .../RNIModalViewModule.swift | 223 +++ .../CAAnimationMulticastDelegate.swift | 28 + .../RNIMulticastDelegate.swift | 27 + .../RNIObjectMetadata+Default.swift | 29 + .../RNIObjectMetadata/RNIObjectMetadata.swift | 14 + ...ontrollerLifeCycleNotifiable+Default.swift | 42 + ...RNIViewControllerLifeCycleNotifiable.swift | 35 + ios/React Native/RNIWeak/RNIWeakArray.swift | 64 + .../RNIWeak/RNIWeakDictionary.swift | 55 + ios/React Native/RNIWeak/RNIWeakRef.swift | 26 + .../CAGradientLayerType+Init.swift | 27 + ios/Temp/Extensions+Init/UIImage+Init.swift | 56 + ios/Temp/Extensions/UIColor+Helpers.swift | 446 ++++++ .../Extensions/UIViewController+Helpers.swift | 24 + ios/Temp/IosUtilities-Bridging-Header.h | 16 + .../RNIJSComponentWillUnmountNotifiable.swift | 18 + ios/Temp/RNICleanup/RNICleanable.swift | 17 + ios/Temp/RNICleanup/RNICleanupMode.swift | 25 + .../RNICleanup/RNIInternalCleanupMode.swift | 40 + ios/Temp/RNIImage/RNIImageGradientMaker.swift | 181 +++ ios/Temp/RNIImage/RNIImageItem.swift | 212 +++ ios/Temp/RNIImage/RNIImageLoadingConfig.swift | 26 + ios/Temp/RNIImage/RNIImageMaker.swift | 59 + ios/Temp/RNIImage/RNIImageOptions.swift | 35 + .../RNIImage/RNIImageRemoteURLMaker.swift | 361 +++++ ios/Temp/RNIImage/RNIImageRequireMaker.swift | 76 + ios/Temp/RNIImage/RNIImageSystemMaker.swift | 113 ++ ios/Temp/RNIImage/RNIImageType.swift | 29 + .../RNIRemoteURLImageLoadingConfig.swift | 52 + .../RNINavigationEventsNotifiable.swift | 14 + ...igationEventsReportingViewController.swift | 65 + ios/Temp/RNIUtilities/RNIUtilities.swift | 165 +++ ios/Temp/RNIUtilities/RNIUtilitiesModule.m | 14 + .../RNIUtilities/RNIUtilitiesModule.swift | 29 + ios/Temp/RNIWrapperView/RNIWrapperView.swift | 273 ++++ .../RNIWrapperViewEventsNotifiable.swift | 16 + .../RNIWrapperView/RNIWrapperViewManager.m | 28 + .../RNIWrapperViewManager.swift | 38 + .../RNIWrapperView/RNIWrapperViewModule.m | 15 + .../RNIWrapperView/RNIWrapperViewModule.swift | 55 + 102 files changed, 9342 insertions(+) create mode 100644 ios/Extensions+Init/CGSize+Init.swift create mode 100644 ios/Extensions+Init/UIBlurEffect+Init.swift create mode 100644 ios/Extensions+Init/UIModalPresentationStyle+Init.swift create mode 100644 ios/Extensions+Init/UISheetPresentationController+Init.swift create mode 100644 ios/Extensions/CAAnimation+Block.swift create mode 100644 ios/Extensions/CAAnimation+Helpers.swift create mode 100644 ios/Extensions/CGRect+Helpers.swift create mode 100644 ios/Extensions/Collection+Helpers.swift create mode 100644 ios/Extensions/Encodable+Helpers.swift create mode 100644 ios/Extensions/FloatingPoint+Clamping.swift create mode 100644 ios/Extensions/KeyWindow+Helpers.swift create mode 100644 ios/Extensions/RNIUtilities+Helpers.swift create mode 100644 ios/Extensions/UIGestureRecognizer+Helpers.swift create mode 100644 ios/Extensions/UIModalTransitionStyle+Helpers.swift create mode 100644 ios/Extensions/UIView+Helpers.swift create mode 100644 ios/Extensions/UIViewController+Swizzling.swift create mode 100644 ios/Extensions/UIWindow+Helpers.swift create mode 100644 ios/Helpers+Utilities/CGSize+Helpers.swift create mode 100644 ios/Helpers+Utilities/WeakElement.swift create mode 100644 ios/React Native/RNIAnimator/RNIAnimator.swift create mode 100644 ios/React Native/RNIAnimator/RNIAnimatorSize.swift create mode 100644 ios/React Native/RNIComputable/RNIComputableOffset.swift create mode 100644 ios/React Native/RNIComputable/RNIComputableSize.swift create mode 100644 ios/React Native/RNIComputable/RNIComputableSizeMode.swift create mode 100644 ios/React Native/RNIComputable/RNIComputableValue.swift create mode 100644 ios/React Native/RNIComputable/RNIComputableValueMode.swift create mode 100644 ios/React Native/RNIComputable/RNIViewMetadata.swift create mode 100644 ios/React Native/RNIDictionarySynthesizable/RNIDictionaryRepresentable.swift create mode 100644 ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable+Default.swift create mode 100644 ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable.swift create mode 100644 ios/React Native/RNIError/RNIBaseError+Helpers.swift create mode 100644 ios/React Native/RNIError/RNIBaseError.swift create mode 100644 ios/React Native/RNIError/RNIError.swift create mode 100644 ios/React Native/RNIError/RNIErrorCodeDefaultable.swift create mode 100644 ios/React Native/RNIError/RNIErrorCodeSynthesizable.swift create mode 100644 ios/React Native/RNIError/RNIGenericErrorCode.swift create mode 100644 ios/React Native/RNIIdentifiable/RNIIdentifiable+Default.swift create mode 100644 ios/React Native/RNIIdentifiable/RNIIdentifiable.swift create mode 100644 ios/React Native/RNIIdentifiable/RNIObjectIdentifier.swift create mode 100644 ios/React Native/RNIModal/RNIModal+Helpers.swift create mode 100644 ios/React Native/RNIModal/RNIModal.swift create mode 100644 ios/React Native/RNIModal/RNIModalCustomSheetDetent.swift create mode 100644 ios/React Native/RNIModal/RNIModalData.swift create mode 100644 ios/React Native/RNIModal/RNIModalError.swift create mode 100644 ios/React Native/RNIModal/RNIModalEventData.swift create mode 100644 ios/React Native/RNIModal/RNIModalFlags.swift create mode 100644 ios/React Native/RNIModal/RNIModalFocusState.swift create mode 100644 ios/React Native/RNIModal/RNIModalManager.swift create mode 100644 ios/React Native/RNIModal/RNIModalPresentationNotifiable.swift create mode 100644 ios/React Native/RNIModal/RNIModalPresentationState.swift create mode 100644 ios/React Native/RNIModal/RNIModalPresentationTrigger.swift create mode 100644 ios/React Native/RNIModal/RNIModalUtilities.swift create mode 100644 ios/React Native/RNIModal/RNIModalWindowMap.swift create mode 100644 ios/React Native/RNIModal/RNIPresentedViewControllerCache.swift create mode 100644 ios/React Native/RNIModalView/RNIModalView.swift create mode 100644 ios/React Native/RNIModalView/RNIModalViewController.swift create mode 100644 ios/React Native/RNIModalView/RNIModalViewError.swift create mode 100644 ios/React Native/RNIModalView/RNIModalViewManager.m create mode 100644 ios/React Native/RNIModalView/RNIModalViewManager.swift create mode 100644 ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapper.swift create mode 100644 ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapperRegistry.swift create mode 100644 ios/React Native/RNIModalViewModule/RNIModalViewModule.m create mode 100644 ios/React Native/RNIModalViewModule/RNIModalViewModule.swift create mode 100644 ios/React Native/RNIMulticastDelegate/CAAnimationMulticastDelegate.swift create mode 100644 ios/React Native/RNIMulticastDelegate/RNIMulticastDelegate.swift create mode 100644 ios/React Native/RNIObjectMetadata/RNIObjectMetadata+Default.swift create mode 100644 ios/React Native/RNIObjectMetadata/RNIObjectMetadata.swift create mode 100644 ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable+Default.swift create mode 100644 ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable.swift create mode 100644 ios/React Native/RNIWeak/RNIWeakArray.swift create mode 100644 ios/React Native/RNIWeak/RNIWeakDictionary.swift create mode 100644 ios/React Native/RNIWeak/RNIWeakRef.swift create mode 100644 ios/Temp/Extensions+Init/CAGradientLayerType+Init.swift create mode 100644 ios/Temp/Extensions+Init/UIImage+Init.swift create mode 100644 ios/Temp/Extensions/UIColor+Helpers.swift create mode 100644 ios/Temp/Extensions/UIViewController+Helpers.swift create mode 100644 ios/Temp/IosUtilities-Bridging-Header.h create mode 100644 ios/Temp/Protocols/RNIJSComponentWillUnmountNotifiable.swift create mode 100644 ios/Temp/RNICleanup/RNICleanable.swift create mode 100644 ios/Temp/RNICleanup/RNICleanupMode.swift create mode 100644 ios/Temp/RNICleanup/RNIInternalCleanupMode.swift create mode 100644 ios/Temp/RNIImage/RNIImageGradientMaker.swift create mode 100644 ios/Temp/RNIImage/RNIImageItem.swift create mode 100644 ios/Temp/RNIImage/RNIImageLoadingConfig.swift create mode 100644 ios/Temp/RNIImage/RNIImageMaker.swift create mode 100644 ios/Temp/RNIImage/RNIImageOptions.swift create mode 100644 ios/Temp/RNIImage/RNIImageRemoteURLMaker.swift create mode 100644 ios/Temp/RNIImage/RNIImageRequireMaker.swift create mode 100644 ios/Temp/RNIImage/RNIImageSystemMaker.swift create mode 100644 ios/Temp/RNIImage/RNIImageType.swift create mode 100644 ios/Temp/RNIImage/RNIRemoteURLImageLoadingConfig.swift create mode 100644 ios/Temp/RNINavigationEventsReporting/RNINavigationEventsNotifiable.swift create mode 100644 ios/Temp/RNINavigationEventsReporting/RNINavigationEventsReportingViewController.swift create mode 100644 ios/Temp/RNIUtilities/RNIUtilities.swift create mode 100644 ios/Temp/RNIUtilities/RNIUtilitiesModule.m create mode 100644 ios/Temp/RNIUtilities/RNIUtilitiesModule.swift create mode 100644 ios/Temp/RNIWrapperView/RNIWrapperView.swift create mode 100644 ios/Temp/RNIWrapperView/RNIWrapperViewEventsNotifiable.swift create mode 100644 ios/Temp/RNIWrapperView/RNIWrapperViewManager.m create mode 100644 ios/Temp/RNIWrapperView/RNIWrapperViewManager.swift create mode 100644 ios/Temp/RNIWrapperView/RNIWrapperViewModule.m create mode 100644 ios/Temp/RNIWrapperView/RNIWrapperViewModule.swift diff --git a/ios/Extensions+Init/CGSize+Init.swift b/ios/Extensions+Init/CGSize+Init.swift new file mode 100644 index 00000000..8b5b33b2 --- /dev/null +++ b/ios/Extensions+Init/CGSize+Init.swift @@ -0,0 +1,21 @@ +// +// CGSize+Init.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/28/23. +// + +import UIKit + +public extension CGSize { + init?(fromDict dict: NSDictionary){ + guard let width = dict["width"] as? NSNumber, + let height = dict["height"] as? NSNumber + else { return nil }; + + self.init( + width: width.doubleValue, + height: height.doubleValue + ); + }; +}; diff --git a/ios/Extensions+Init/UIBlurEffect+Init.swift b/ios/Extensions+Init/UIBlurEffect+Init.swift new file mode 100644 index 00000000..54f78cb3 --- /dev/null +++ b/ios/Extensions+Init/UIBlurEffect+Init.swift @@ -0,0 +1,132 @@ +// +// UIBlurEffect+Init.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/23/23. +// + +import UIKit + +extension UIBlurEffect.Style: CaseIterable, CustomStringConvertible { + + /// The available `UIBlurEffect.Style` that can be used based on the current + /// platform version + /// + public static var availableStyles: [UIBlurEffect.Style] { + var styles: [UIBlurEffect.Style] = [ + .light, + .extraLight, + .dark, + ]; + + if #available(iOS 10.0, *) { + styles.append(contentsOf: [ + .regular, + .prominent, + ]); + }; + + if #available(iOS 13.0, *) { + styles.append(contentsOf: [ + .systemUltraThinMaterial, + .systemThinMaterial, + .systemMaterial, + .systemThickMaterial, + .systemChromeMaterial, + .systemMaterialLight, + .systemThinMaterialLight, + .systemUltraThinMaterialLight, + .systemThickMaterialLight, + .systemChromeMaterialLight, + .systemChromeMaterialDark, + .systemMaterialDark, + .systemThickMaterialDark, + .systemThinMaterialDark, + .systemUltraThinMaterialDark, + ]); + }; + + return styles; + }; + + // MARK: - CaseIterable + // -------------------- + + public static var allCases: [UIBlurEffect.Style] { + return self.availableStyles; + }; + + // MARK: - CustomStringConvertible + // ------------------------------- + + /// Note:2023-03-23-23-14-57 + /// + /// * `UIBlurEffect.Style` is an objc enum, and as such, it's actually raw + /// value internally is `Int`. + /// + /// * As such, `String(describing:)` a ` UIBlurEffect.Style` enum value + /// outputs an `Int`. + /// + /// * Because of this, we have to manually map out the enum values to a string + /// representation. + /// + public var description: String { + switch self { + // Adaptable Styles + case .systemUltraThinMaterial: return "systemUltraThinMaterial"; + case .systemThinMaterial : return "systemThinMaterial"; + case .systemMaterial : return "systemMaterial"; + case .systemThickMaterial : return "systemThickMaterial"; + case .systemChromeMaterial : return "systemChromeMaterial"; + + // Light Styles + case .systemMaterialLight : return "systemMaterialLight"; + case .systemThinMaterialLight : return "systemThinMaterialLight"; + case .systemUltraThinMaterialLight: return "systemUltraThinMaterialLight"; + case .systemThickMaterialLight : return "systemThickMaterialLight"; + case .systemChromeMaterialLight : return "systemChromeMaterialLight"; + + // Dark Styles + case .systemChromeMaterialDark : return "systemChromeMaterialDark"; + case .systemMaterialDark : return "systemMaterialDark"; + case .systemThickMaterialDark : return "systemThickMaterialDark"; + case .systemThinMaterialDark : return "systemThinMaterialDark"; + case .systemUltraThinMaterialDark: return "systemUltraThinMaterialDark"; + + // Additional Styles + case .regular : return "regular"; + case .prominent : return "prominent"; + case .light : return "light"; + case .extraLight: return "extraLight"; + case .dark : return "dark"; + + @unknown default: return ""; + }; + }; + + // MARK: - Init + // ------------ + + init?(string: String){ + + /// Note:2023-03-23-23-21-21 + /// + /// * Normally, a simple `switch` + `case "foo": self = foo` would suffice, + /// (especially since it's O(1) access), but the usable enum values depend + /// on the platform version. + /// + /// * The useable enums are stored in `availableStyles`, and is used to + /// communicate to JS the available enum values. + /// + /// * As such, we might as well re-use `availableStyles` for the parsing + /// logic (even if it's less efficient). + /// + let style = Self.allCases.first{ + $0.description == string + }; + + guard let style = style else { return nil }; + self = style; + }; +}; + diff --git a/ios/Extensions+Init/UIModalPresentationStyle+Init.swift b/ios/Extensions+Init/UIModalPresentationStyle+Init.swift new file mode 100644 index 00000000..d27c1c81 --- /dev/null +++ b/ios/Extensions+Init/UIModalPresentationStyle+Init.swift @@ -0,0 +1,81 @@ +// +// UIModalPresentationStyle+Init.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/24/23. +// + +import UIKit + +extension UIModalPresentationStyle: CaseIterable, CustomStringConvertible { + + /// The available `UIModalPresentationStyle` that can be used based on the + /// current platform version + /// + public static var availableStyles: [UIModalPresentationStyle] { + var styles: [UIModalPresentationStyle] = [ + .fullScreen, + .pageSheet, + .formSheet, + .currentContext, + .custom, + .overFullScreen, + .overCurrentContext, + .popover, + ]; + + if #available(iOS 13.0, *) { + styles.append(.automatic); + }; + + #if !os(iOS) + styles.append(.blurOverFullScreen); + #endif + + return styles; + }; + + + public static var allCases: [UIModalPresentationStyle] { + return self.availableStyles; + }; + + // MARK: - CustomStringConvertible + // ------------------------------- + + /// See: Note:2023-03-23-23-14-57 + public var description: String { + switch self { + case .automatic : return "automatic"; + case .none : return "none"; + case .fullScreen : return "fullScreen"; + case .pageSheet : return "pageSheet"; + case .formSheet : return "formSheet"; + case .currentContext : return "currentContext"; + case .custom : return "custom"; + case .overFullScreen : return "overFullScreen"; + case .overCurrentContext: return "overCurrentContext"; + case .popover : return "popover"; + + #if !os(iOS) + case .blurOverFullScreen: return "blurOverFullScreen"; + #endif + + @unknown default: return ""; + }; + }; + + + // MARK: - Init + // ------------ + + init?(string: String){ + /// See: Note:2023-03-23-23-21-21 + let style = Self.allCases.first{ + $0.description == string + }; + + guard let style = style else { return nil }; + self = style; + }; +}; diff --git a/ios/Extensions+Init/UISheetPresentationController+Init.swift b/ios/Extensions+Init/UISheetPresentationController+Init.swift new file mode 100644 index 00000000..a24a3383 --- /dev/null +++ b/ios/Extensions+Init/UISheetPresentationController+Init.swift @@ -0,0 +1,67 @@ +// +// UISheetPresentationController+Init.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/21/23. +// + +import UIKit + + +@available(iOS 15.0, *) +extension UISheetPresentationController.Detent { + + // 2, + // _identifier=com.apple.UIKit.medium + // > + // + // 1, + // _identifier=com.apple.UIKit.large + // > + + static func fromString( + _ string: String + ) -> UISheetPresentationController.Detent? { + + switch string { + case "medium": return .medium(); + case "large" : return .large(); + + default: return nil; + }; + }; +}; + +@available(iOS 15.0, *) +extension UISheetPresentationController.Detent.Identifier: + CustomStringConvertible { + + public var description: String { + switch self { + case .medium: return "medium"; + case .large : return "large"; + + default: return self.rawValue; + }; + }; + + init?(fromSystemIdentifierString string: String) { + switch string { + case "medium": self = .medium; + case "large" : self = .large; + + default: return nil; + }; + }; + + init(fromString string: String) { + if let systemIdentifier = Self.init(fromSystemIdentifierString: string) { + self = systemIdentifier; + + } else { + self.init(string); + }; + }; +}; diff --git a/ios/Extensions/CAAnimation+Block.swift b/ios/Extensions/CAAnimation+Block.swift new file mode 100644 index 00000000..49cfbf2a --- /dev/null +++ b/ios/Extensions/CAAnimation+Block.swift @@ -0,0 +1,61 @@ +// +// CAAnimation+Block.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/1/23. +// + +import UIKit + + +public class CAAnimationBlockDelegate: NSObject, CAAnimationDelegate { + + public typealias StartBlock = (CAAnimation) -> (); + public typealias EndBlock = (CAAnimation, Bool) -> (); + + var onStartBlock: StartBlock? + var onEndBlock: EndBlock? + + public func animationDidStart(_ anim: CAAnimation) { + self.onStartBlock?(anim); + }; + + public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { + self.onEndBlock?(anim, flag); + }; +}; + +public extension CAAnimation { + + private var multicastDelegate: CAAnimationMulticastDelegate { + guard let delegate = self.delegate else { + return CAAnimationMulticastDelegate(); + }; + + guard let multicastDelegate = delegate as? CAAnimationMulticastDelegate else { + let multicastDelegate = CAAnimationMulticastDelegate(); + multicastDelegate.emitter.add(delegate); + + self.speed = 0; + + self.delegate = multicastDelegate; + return multicastDelegate; + }; + + return multicastDelegate; + }; + + func startBlock(_ callback: @escaping CAAnimationBlockDelegate.StartBlock) { + let blockDelegate = CAAnimationBlockDelegate(); + self.multicastDelegate.emitter.add(blockDelegate); + + blockDelegate.onStartBlock = callback; + }; + + func endBlock(_ callback: @escaping CAAnimationBlockDelegate.EndBlock) { + let blockDelegate = CAAnimationBlockDelegate(); + self.multicastDelegate.emitter.add(blockDelegate); + + blockDelegate.onEndBlock = callback; + }; +}; diff --git a/ios/Extensions/CAAnimation+Helpers.swift b/ios/Extensions/CAAnimation+Helpers.swift new file mode 100644 index 00000000..cc91ee1d --- /dev/null +++ b/ios/Extensions/CAAnimation+Helpers.swift @@ -0,0 +1,17 @@ +// +// CAAnimation+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/1/23. +// + +import UIKit + +public extension CAAnimation { + func waitUntiEnd(_ block: @escaping () -> Void){ + DispatchQueue.main.asyncAfter( + deadline: .now() + self.duration, + execute: block + ); + }; +}; diff --git a/ios/Extensions/CGRect+Helpers.swift b/ios/Extensions/CGRect+Helpers.swift new file mode 100644 index 00000000..ed067e30 --- /dev/null +++ b/ios/Extensions/CGRect+Helpers.swift @@ -0,0 +1,54 @@ +// +// CGSize+Helpers.swift +// swift-programmatic-modal +// +// Created by Dominic Go on 5/19/23. +// + +import UIKit + +extension CGRect { + mutating func setPoint( + minX: CGFloat? = nil, + minY: CGFloat? = nil + ){ + self.origin = CGPoint( + x: minX ?? self.minX, + y: minY ?? self.minY + ); + }; + + mutating func setPoint( + midX: CGFloat? = nil, + midY: CGFloat? = nil + ){ + let newX: CGFloat = { + guard let midX = midX else { return self.minX }; + return midX - (self.width / 2); + }(); + + let newY: CGFloat = { + guard let midY = midY else { return self.minY }; + return midY - (self.height / 2); + }(); + + self.origin = CGPoint(x: newX, y: newY); + }; + + mutating func setPoint( + maxX: CGFloat? = nil, + maxY: CGFloat? = nil + ){ + let newX: CGFloat = { + guard let maxX = maxX else { return self.minX }; + return maxX - self.width; + }(); + + let newY: CGFloat = { + guard let maxY = maxY else { return self.minY }; + return maxY - self.height; + }(); + + self.origin = CGPoint(x: newX, y: newY); + }; +}; diff --git a/ios/Extensions/Collection+Helpers.swift b/ios/Extensions/Collection+Helpers.swift new file mode 100644 index 00000000..177868c2 --- /dev/null +++ b/ios/Extensions/Collection+Helpers.swift @@ -0,0 +1,41 @@ +// +// Collection+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/10/23. +// + +import UIKit + +extension Collection { + + public var secondToLast: Element? { + self[safeIndex: self.index(self.indices.endIndex, offsetBy: -2)]; + }; + + public func isOutOfBounds(forIndex index: Index) -> Bool { + return index < self.indices.startIndex || index >= self.indices.endIndex; + }; + + /// Returns the element at the specified index if it is within bounds, + /// otherwise nil. + public subscript(safeIndex index: Index) -> Element? { + return self.isOutOfBounds(forIndex: index) ? nil : self[index]; + }; +}; + +extension MutableCollection { + subscript(safeIndex index: Index) -> Element? { + get { + return self.isOutOfBounds(forIndex: index) ? nil : self[index]; + } + + set { + guard let newValue = newValue, + !self.isOutOfBounds(forIndex: index) + else { return }; + + self[index] = newValue; + } + }; +}; diff --git a/ios/Extensions/Encodable+Helpers.swift b/ios/Extensions/Encodable+Helpers.swift new file mode 100644 index 00000000..b4490c5a --- /dev/null +++ b/ios/Extensions/Encodable+Helpers.swift @@ -0,0 +1,31 @@ +// +// Encodable+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/2/23. +// + +import UIKit + + +public extension Encodable { + + var asJsonData: Data? { + let encoder = JSONEncoder(); + encoder.outputFormatting = .prettyPrinted; + encoder.dateEncodingStrategy = .iso8601; + + return try? encoder.encode(self); + }; + + var asDictionary : [String: Any]? { + guard let jsonData = self.asJsonData, + let json = try? JSONSerialization.jsonObject( + with: jsonData, + options: [] + ) + else { return nil }; + + return json as? [String: Any]; + }; +}; diff --git a/ios/Extensions/FloatingPoint+Clamping.swift b/ios/Extensions/FloatingPoint+Clamping.swift new file mode 100644 index 00000000..c685ba9b --- /dev/null +++ b/ios/Extensions/FloatingPoint+Clamping.swift @@ -0,0 +1,31 @@ +// +// FloatingPoint+Helpers.swift +// swift-programmatic-modal +// +// Created by Dominic Go on 5/19/23. +// + +import UIKit + +extension FloatingPoint { + public func clamped( + min lowerBound: Self? = nil, + max upperBound: Self? = nil + ) -> Self { + var clampedValue = self; + + if let upperBound = upperBound { + clampedValue = min(clampedValue, upperBound); + }; + + if let lowerBound = lowerBound { + clampedValue = max(clampedValue, lowerBound); + }; + + return clampedValue; + }; + + public func clamped(minMax: Self) -> Self { + self.clamped(min: -minMax, max: minMax); + }; +}; diff --git a/ios/Extensions/KeyWindow+Helpers.swift b/ios/Extensions/KeyWindow+Helpers.swift new file mode 100644 index 00000000..72933909 --- /dev/null +++ b/ios/Extensions/KeyWindow+Helpers.swift @@ -0,0 +1,21 @@ +// +// KeyWindow+Helpers.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 6/26/20. +// + +import UIKit + +extension UIWindow { + + /// TODO:2023-03-24-01-14-26 - Remove/Replace `UIWindow.key` + static var key: UIWindow? { + if #available(iOS 13, *) { + return UIApplication.shared.windows.first { $0.isKeyWindow }; + + } else { + return UIApplication.shared.keyWindow; + }; + }; +}; diff --git a/ios/Extensions/RNIUtilities+Helpers.swift b/ios/Extensions/RNIUtilities+Helpers.swift new file mode 100644 index 00000000..1fb0f17a --- /dev/null +++ b/ios/Extensions/RNIUtilities+Helpers.swift @@ -0,0 +1,158 @@ +// +// RNIUtilities+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +/// TODO:2023-03-20-21-29-36 - Move to `RNIUtilities` +extension RNIUtilities { + + static func swizzleExchangeMethods( + defaultSelector: Selector, + newSelector: Selector, + forClass class: AnyClass + ) { + let defaultInstace = + class_getInstanceMethod(`class`.self, defaultSelector); + + let newInstance = + class_getInstanceMethod(`class`.self, newSelector); + + guard let defaultInstance = defaultInstace, + let newInstance = newInstance + else { return }; + + method_exchangeImplementations(defaultInstance, newInstance); + }; + + static func getFirstMatchingView( + forNativeID targetNativeID: String, + startingView rootView: UIView + ) -> UIView? { + + for view in rootView.subviews { + if let viewNativeID = view.nativeID, + viewNativeID == targetNativeID { + + return view; + }; + + let matchingView = Self.getFirstMatchingView( + forNativeID: targetNativeID, + startingView: view + ); + + guard let matchingView = matchingView else { continue }; + return matchingView; + }; + + return nil; + }; + + public static func getWindows() -> [UIWindow] { + var windows: [UIWindow] = []; + + #if swift(>=5.5) + // Version: Swift 5.5 and newer - iOS 15 and newer + guard #available(iOS 13.0, *) else { return [] }; + + for scene in UIApplication.shared.connectedScenes { + guard let windowScene = scene as? UIWindowScene else { continue }; + windows += windowScene.windows; + }; + + #elseif swift(>=5) + // Version: Swift 5.4 and below - iOS 14.5 and below + // Note: 'windows' was deprecated in iOS 15.0+ + + // first element is the "key window" + if let keyWindow = + UIApplication.shared.windows.first(where: { $0.isKeyWindow }) { + + windows.append(keyWindow); + }; + + UIApplication.shared.windows.forEach { + // skip if already added + guard !windows.contains($0) else { return }; + windows.append($0); + }; + + #elseif swift(>=4) + // Version: Swift 4 and below - iOS 12.4 and below + // Note: `keyWindow` was deprecated in iOS 13.0+ + + // first element is the "key window" + if let keyWindow = UIApplication.shared.keyWindow { + windows.append(keyWindow); + }; + + UIApplication.shared.windows.forEach { + // skip if already added + guard !windows.contains($0) else { return }; + windows.append($0); + }; + + #else + // Version: Swift 3.1 and below - iOS 10.3 and below + // Note: 'sharedApplication' has been renamed to 'shared' + guard let appDelegate = + UIApplication.sharedApplication().delegate as? AppDelegate, + + let window = appDelegate.window + else { return [] }; + + return windows.append(window); + #endif + + return windows; + }; + + public static func getRootViewController( + for window: UIWindow? = nil + ) -> UIViewController? { + + if let window = window { + return window.rootViewController; + }; + + return Self.getWindows().first?.rootViewController; + }; + + public static func getPresentedViewControllers( + for window: UIWindow? = nil + ) -> [UIViewController] { + guard let rootVC = Self.getRootViewController(for: window) else { + #if DEBUG + print( + "Error - RNIModalManager.getTopMostPresentedVC" + + " - arg window isNil: '\(window == nil)'" + + " - Could not get root view controller" + ); + #endif + return []; + }; + + var presentedVCList: [UIViewController] = [rootVC]; + + // climb the vc hierarchy to find the topmost presented vc + while true { + guard let topVC = presentedVCList.last, + let presentedVC = topVC.presentedViewController + else { break }; + + presentedVCList.append(presentedVC); + }; + + return presentedVCList; + }; + + public static func getTopmostPresentedViewController( + for window: UIWindow? = nil + ) -> UIViewController? { + return Self.getPresentedViewControllers(for: window).last; + }; +}; diff --git a/ios/Extensions/UIGestureRecognizer+Helpers.swift b/ios/Extensions/UIGestureRecognizer+Helpers.swift new file mode 100644 index 00000000..2615c673 --- /dev/null +++ b/ios/Extensions/UIGestureRecognizer+Helpers.swift @@ -0,0 +1,23 @@ +// +// UIGestureRecognizer+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/22/23. +// + +import UIKit + +extension UIGestureRecognizer.State: CustomStringConvertible { + public var description: String { + switch self { + case .possible : return "possible"; + case .began : return "began"; + case .changed : return "changed"; + case .ended : return "ended"; + case .cancelled: return "cancelled"; + case .failed : return "failed"; + + @unknown default: return ""; + }; + }; +}; diff --git a/ios/Extensions/UIModalTransitionStyle+Helpers.swift b/ios/Extensions/UIModalTransitionStyle+Helpers.swift new file mode 100644 index 00000000..9799d40d --- /dev/null +++ b/ios/Extensions/UIModalTransitionStyle+Helpers.swift @@ -0,0 +1,34 @@ +// +// UIModalTransitionStyle+Helpers.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 6/29/20. +// + +import UIKit + +extension UIModalTransitionStyle: CaseIterable { + public static var allCases: [UIModalTransitionStyle] { + return [ + .coverVertical, + .crossDissolve, + .flipHorizontal, + .partialCurl, + ]; + }; + + public func stringDescription() -> String { + switch self { + case .coverVertical : return "coverVertical"; + case .flipHorizontal: return "flipHorizontal"; + case .crossDissolve : return "crossDissolve"; + case .partialCurl : return "partialCurl"; + + @unknown default: return ""; + }; + }; + + public static func fromString(_ string: String) -> UIModalTransitionStyle? { + return self.allCases.first{ $0.stringDescription() == string }; + }; +}; diff --git a/ios/Extensions/UIView+Helpers.swift b/ios/Extensions/UIView+Helpers.swift new file mode 100644 index 00000000..139cb5e2 --- /dev/null +++ b/ios/Extensions/UIView+Helpers.swift @@ -0,0 +1,60 @@ +import UIKit + +/// TODO:2023-03-24-01-14-26 - Move `UIView+Helpers` extension to +/// `react-native-utilities` +/// +extension UIView { + public var parentViewController: UIViewController? { + var parentResponder: UIResponder? = self; + + while parentResponder != nil { + parentResponder = parentResponder!.next + if let viewController = parentResponder as? UIViewController { + return viewController; + }; + }; + + return nil; + }; + + /// Remove all ancestor constraints that are affecting this view instance + /// + /// Note: 2023-03-24-00-39-51 + /// + /// * From: https://stackoverflow.com/questions/24418884/remove-all-constraints-affecting-a-uiview + /// + /// * After it's done executing, your view remains where it was because it + /// creates autoresizing constraints. + /// + /// * When I don't do this the view usually disappears. + /// + /// * Additionally, it doesn't just remove constraints from it's superview, + /// but in addition, it also climbs the view hierarchy, and removes all the + /// constraints affecting the current view instance that came from an + /// ancestor view. + /// + public func removeAllAncestorConstraints() { + var ancestorView = self.superview; + + // Climb the view hierarchy until there are no more parent views... + while ancestorView != nil { + for ancestorConstraint in ancestorView!.constraints { + + let constraintItems = [ + ancestorConstraint.firstItem, + ancestorConstraint.secondItem + ]; + + constraintItems.forEach { + guard ($0 as? UIView) === self else { return }; + ancestorView?.removeConstraint(ancestorConstraint); + }; + + ancestorView = ancestorView?.superview; + }; + }; + + self.removeConstraints(self.constraints); + self.translatesAutoresizingMaskIntoConstraints = true + }; +}; diff --git a/ios/Extensions/UIViewController+Swizzling.swift b/ios/Extensions/UIViewController+Swizzling.swift new file mode 100644 index 00000000..22457f33 --- /dev/null +++ b/ios/Extensions/UIViewController+Swizzling.swift @@ -0,0 +1,235 @@ +// +// RNIModalSwizzledViewController.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/11/23. +// + +import UIKit + +extension UIViewController { + + // MARK: - Static - Swizzling-Related + // ---------------------------------- + + static var isSwizzled = false; + + internal static func swizzleMethods() { + guard RNIModalFlagsShared.shouldSwizzleViewControllers else { return }; + + #if DEBUG + print( + "Log - UIViewController+Swizzling" + + " - UIViewController.swizzleMethods invoked" + ); + #endif + + RNIUtilities.swizzleExchangeMethods( + defaultSelector: #selector(Self.present(_:animated:completion:)), + newSelector: #selector(Self._swizzled_present(_:animated:completion:)), + forClass: UIViewController.self + ); + + RNIUtilities.swizzleExchangeMethods( + defaultSelector: #selector(Self.dismiss(animated:completion:)), + newSelector: #selector(Self._swizzled_dismiss(animated:completion:)), + forClass: UIViewController.self + ); + + self.isSwizzled.toggle(); + }; + + // MARK: - Helpers - Static + // ------------------------ + + @discardableResult + static private func registerIfNeeded( + forViewController vc: UIViewController + ) -> RNIModalViewControllerWrapper? { + + let shouldWrapVC = vc is RNIModalViewController + ? RNIModalFlagsShared.shouldWrapAllViewControllers + : true; + + /// If the arg `vc` is a `RNIModalViewController` instance, then we don't + /// need to wrap the current instance inside a + /// `RNIModalViewControllerWrapper` since it will already notify + /// `RNIModalManager` of modal-related events... + /// + guard shouldWrapVC else { return nil }; + + let modalWrapper: RNIModalViewControllerWrapper = { + /// A - Wrapper already exists for arg `vc`, so return the matching + /// wrapper instance. + /// + if let modalWrapper = RNIModalViewControllerWrapperRegistry.get( + forViewController: vc + ) { + return modalWrapper; + }; + + /// B - Wrapper does not exists for arg `vc`, so create a new wrapper + /// instance. + /// + let newModalWrapper = RNIModalViewControllerWrapper(viewController: vc); + + RNIModalViewControllerWrapperRegistry.set( + forViewController: vc, + newModalWrapper + ); + return newModalWrapper; + }(); + + return modalWrapper; + }; + + // MARK: - Helpers - Computed Properties + // ------------------------------------- + + /// Get the associated `RNIModalViewControllerWrapper` instance for the + /// current view controller + /// + var modalWrapper: RNIModalViewControllerWrapper? { + RNIModalViewControllerWrapperRegistry.get(forViewController: self); + }; + + // MARK: - Helpers - Functions + // --------------------------- + + private func getPresentedModalToNotify( + _ presentedVC: UIViewController? = nil + ) -> (any RNIModal)? { + + let presentedModal = RNIModalUtilities.getPresentedModal( + forPresentingViewController: self, + presentedViewController: presentedVC + ); + + return RNIModalFlagsShared.shouldSwizzledViewControllerNotifyAll + ? presentedModal + : presentedModal as? RNIModalViewControllerWrapper; + }; + + private func registerOrInitialize( + _ viewControllerToPresent: UIViewController + ){ + let presentingWrapper = Self.registerIfNeeded(forViewController: self); + + presentingWrapper?.modalViewController = viewControllerToPresent; + presentingWrapper?.presentingViewController = self; + + let presentedWrapper = + Self.registerIfNeeded(forViewController: viewControllerToPresent); + + presentedWrapper?.presentingViewController = self; + }; + + + private func notifyOnModalWillDismiss() -> (() -> Void)? { + guard let presentedModal = self.getPresentedModalToNotify() + else { return nil }; + + presentedModal.notifyWillDismiss(); + + return { + if presentedModal.computedIsModalInFocus { + presentedModal.notifyDidPresent(); + + } else { + presentedModal.notifyDidDismiss(); + }; + }; + }; + + // MARK: - Swizzled Functions + // -------------------------- + + @objc fileprivate func _swizzled_present( + _ viewControllerToPresent: UIViewController, + animated flag: Bool, + completion: (() -> Void)? = nil + ) { + #if DEBUG + print( + "Log - UIViewController+Swizzling" + + " - UIViewController._swizzled_present invoked" + + " - arg viewControllerToPresent: \(viewControllerToPresent)" + + " - arg animated: \(flag)" + ); + #endif + + self.registerOrInitialize(viewControllerToPresent); + + let presentedModal = self.getPresentedModalToNotify(viewControllerToPresent); + presentedModal?.notifyWillPresent(); + + // call original impl. + self._swizzled_present(viewControllerToPresent, animated: flag) { + #if DEBUG + print( + "Log - UIViewController+Swizzling" + + " - UIViewController._swizzled_present" + + " - completion invoked" + ); + #endif + + presentedModal?.notifyDidPresent(); + completion?(); + }; + }; + + @objc fileprivate func _swizzled_dismiss( + animated flag: Bool, + completion: (() -> Void)? = nil + ) { + #if DEBUG + print( + "Log - UIViewController+Swizzling" + + " - UIViewController._swizzled_dismiss invoked" + + " - arg animated: \(flag)" + + " - self.presentedViewController: \(String(describing: presentedViewController))" + ); + #endif + + let notifyOnModalDidDismiss = self.notifyOnModalWillDismiss(); + + // call original impl. + self._swizzled_dismiss(animated: flag) { + #if DEBUG + print( + "Log - UIViewController+Swizzling" + + " - UIViewController._swizzled_dismiss" + + " - completion invoked" + ); + #endif + + notifyOnModalDidDismiss?(); + completion?(); + }; + }; +}; + +// MARK: - Extensions - Helpers +// ---------------------------- + +fileprivate extension RNIModalPresentationNotifying where Self: RNIModal { + func notifyWillPresent() { + guard let delegate = modalPresentationNotificationDelegate else { return }; + delegate.notifyOnModalWillShow(sender: self); + }; + + func notifyDidPresent(){ + guard let delegate = modalPresentationNotificationDelegate else { return }; + delegate.notifyOnModalDidShow(sender: self); + }; + + func notifyWillDismiss(){ + guard let delegate = modalPresentationNotificationDelegate else { return }; + delegate.notifyOnModalWillHide(sender: self); + }; + + func notifyDidDismiss(){ + guard let delegate = modalPresentationNotificationDelegate else { return }; + delegate.notifyOnModalDidHide(sender: self); + }; +}; diff --git a/ios/Extensions/UIWindow+Helpers.swift b/ios/Extensions/UIWindow+Helpers.swift new file mode 100644 index 00000000..fa261a7f --- /dev/null +++ b/ios/Extensions/UIWindow+Helpers.swift @@ -0,0 +1,12 @@ +// +// UIWindow+WindowMetadata.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/30/23. +// + +import UIKit + +extension UIWindow: RNIObjectMetadata, RNIIdentifiable { + public static var synthesizedIdPrefix = "window-id-"; +}; diff --git a/ios/Helpers+Utilities/CGSize+Helpers.swift b/ios/Helpers+Utilities/CGSize+Helpers.swift new file mode 100644 index 00000000..389a0c4d --- /dev/null +++ b/ios/Helpers+Utilities/CGSize+Helpers.swift @@ -0,0 +1,14 @@ +// +// CGSize+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/29/23. +// + +import UIKit + +public extension CGSize { + var isZero: Bool { + self == .zero || (self.width == 0 && self.height == 0); + }; +}; diff --git a/ios/Helpers+Utilities/WeakElement.swift b/ios/Helpers+Utilities/WeakElement.swift new file mode 100644 index 00000000..d480d4f6 --- /dev/null +++ b/ios/Helpers+Utilities/WeakElement.swift @@ -0,0 +1,13 @@ +// +// WeakElement.swift +// RNSwiftReviewer +// +// Created by Dominic Go on 8/15/20. +// + +import UIKit + + +public class WeakElement { + private(set) weak var value: Element?; +}; diff --git a/ios/React Native/RNIAnimator/RNIAnimator.swift b/ios/React Native/RNIAnimator/RNIAnimator.swift new file mode 100644 index 00000000..599b8a93 --- /dev/null +++ b/ios/React Native/RNIAnimator/RNIAnimator.swift @@ -0,0 +1,243 @@ +// +// RNIAnimator.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/19/23. +// + +import UIKit + + +public class RNIAnimator { + + // MARK: - Embedded Types + // ---------------------- + + typealias EasingFunction = (_ timing: CGFloat) -> CGFloat; + + public class EasingFunctions { + + static func lerp( + valueStart: CGFloat, + valueEnd: CGFloat, + percent: CGFloat + ) -> CGFloat { + let valueDelta = valueEnd - valueStart; + let valueProgress = valueDelta * percent + return valueStart + valueProgress; + }; + + static func linear(_ time: CGFloat) -> CGFloat { + return time; + }; + + static func easeIn(_ time: CGFloat) -> CGFloat { + return time * time; + }; + }; + + public enum Easing: String { + case linear; + case easeIn; + + var easingFunction: EasingFunction { + switch self { + case .linear: return EasingFunctions.linear; + case .easeIn: return EasingFunctions.easeIn; + }; + }; + }; + + // MARK: - Properties + // ------------------ + + private var displayLink: CADisplayLink?; + + public var easing: Easing = .linear; + + private(set) public var timeStart: CFTimeInterval = 0; + private(set) public var timeEnd: CFTimeInterval = 0; + + private(set) public var timeCurrent: CFTimeInterval = 0; + private(set) public var timeElapsed: CFTimeInterval = 0; + + private(set) public var progress: CGFloat = 0; + private(set) public var duration: CFTimeInterval; + + public let animatedValuesSize: Int; + + private(set) public var animatedValuesStart: [CGFloat]; + private(set) public var animatedValuesEnd: [CGFloat]; + + private(set) public var animatedValuesPrev: [CGFloat] = []; + private(set) public var animatedValuesCurrent: [CGFloat] = []; + + public var allowAnimatedValueToRegress = true; + + // MARK: - Properties - Stored Functions + // ------------------------------------- + + private var applyPendingUpdates: (() -> Void)? = nil; + + public var onAnimatedValueChange: ((_ animatedValues: [CGFloat]) -> Void)?; + public var onAnimationCompletion: (() -> Void)?; + + // MARK: - Properties - Computed + // ----------------------------- + + public var isFinished: Bool { + self.animatedValuesCurrent.enumerated().allSatisfy { + self.animatedValuesStart[$0.offset] < self.animatedValuesEnd[$0.offset] + ? $0.element >= self.animatedValuesEnd[$0.offset] + : $0.element <= self.animatedValuesEnd[$0.offset] + }; + }; + + public var isAnimating: Bool { + self.displayLink != nil + }; + + // MARK: - Init + // ------------ + + public init?( + durationSeconds: CFTimeInterval, + animatedValuesStart: [CGFloat], + animatedValuesEnd: [CGFloat], + onAnimatedValueChange: ((_ animatedValues: [CGFloat]) -> Void)? = nil, + onAnimationCompletion: (() -> Void)? = nil + ) { + guard animatedValuesStart.count == animatedValuesEnd.count else { + return nil; + }; + + self.duration = durationSeconds; + + self.animatedValuesStart = animatedValuesStart; + self.animatedValuesEnd = animatedValuesEnd; + + let arraySize = animatedValuesStart.count; + self.animatedValuesSize = arraySize; + + self.animatedValuesCurrent = [CGFloat](repeating: 0, count: arraySize); + self.animatedValuesPrev = [CGFloat](repeating: 0, count: arraySize); + + self.onAnimatedValueChange = onAnimatedValueChange; + self.onAnimationCompletion = onAnimationCompletion; + }; + + // MARK: - Functions + // ----------------- + + @objc private func onDisplayLinkDidFire(_ displayLink: CADisplayLink){ + + self.timeCurrent = CACurrentMediaTime(); + self.timeElapsed = self.timeCurrent - self.timeStart; + + self.progress = self.timeElapsed / self.duration; + + var didChange = false; + + for index in 0 ..< self.animatedValuesSize { + + let animatedValueNextRaw = Self.EasingFunctions.lerp( + valueStart: self.animatedValuesStart[index], + valueEnd: self.animatedValuesEnd[index], + percent: self.easing.easingFunction(self.progress) + ); + + let animatedValuesStart = self.animatedValuesStart[index]; + let animatedValueEnd = self.animatedValuesEnd[index]; + + // clamp + let animatedValueNext: CGFloat = { + // E.g. 50 -> 100 + if animatedValuesStart <= animatedValueEnd { + return animatedValueNextRaw > animatedValueEnd + ? animatedValueEnd + : animatedValueNextRaw; + + } else { + // E.g. 100 -> 50 + return animatedValueNextRaw < animatedValueEnd + ? animatedValueEnd + : animatedValueNextRaw; + }; + }(); + + let animatedValuePrev = self.animatedValuesPrev[index]; + let animatedValueCurrent = self.animatedValuesCurrent[index]; + + let shouldUpdate = self.allowAnimatedValueToRegress + ? true + : animatedValueNext > animatedValueCurrent; + + guard shouldUpdate else { continue }; + + self.animatedValuesPrev[index] = self.animatedValuesCurrent[index]; + self.animatedValuesCurrent[index] = animatedValueNext; + + if !didChange { + didChange = animatedValuePrev != animatedValueNext; + }; + }; + + if didChange { + self.onAnimatedValueChange?(self.animatedValuesCurrent); + }; + + if self.isFinished { + self.stop(); + self.onAnimationCompletion?(); + }; + + self.applyPendingUpdates?(); + }; + + // MARK: - Functions - Public + // -------------------------- + + public func start(){ + self.stop(); + + self.timeCurrent = CACurrentMediaTime(); + self.timeStart = self.timeCurrent; + self.timeEnd = self.timeCurrent + self.duration; + + let displayLink = CADisplayLink( + target: self, + selector: #selector(Self.onDisplayLinkDidFire(_:)) + ); + + displayLink.add(to: .current, forMode:.default); + self.displayLink = displayLink; + }; + + public func stop() { + self.displayLink?.invalidate(); + self.displayLink = nil; + }; + + public func update( + animatedValuesEnd: [CGFloat], + duration: CGFloat? = nil + ){ + self.applyPendingUpdates = { [unowned self] in + self.animatedValuesStart = self.animatedValuesCurrent; + self.animatedValuesEnd = animatedValuesEnd; + + self.timeCurrent = CACurrentMediaTime(); + + if let newDuration = duration { + self.duration = newDuration; + + } else { + let timeRemaining = self.duration - self.timeElapsed; + self.duration = timeRemaining; + }; + + self.timeStart = self.timeCurrent; + self.timeEnd = self.timeCurrent + self.duration; + }; + }; +}; diff --git a/ios/React Native/RNIAnimator/RNIAnimatorSize.swift b/ios/React Native/RNIAnimator/RNIAnimatorSize.swift new file mode 100644 index 00000000..bcef9d9c --- /dev/null +++ b/ios/React Native/RNIAnimator/RNIAnimatorSize.swift @@ -0,0 +1,54 @@ +// +// RNIAnimatorSize.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/19/23. +// + +import UIKit + +fileprivate extension CGSize { + init(array: [CGFloat]) { + self = CGSize(width: array[0], height: array[1]); + }; + + var array: [CGFloat] { + return [ + self.width, + self.height + ]; + }; +}; + +public class RNIAnimatorSize: RNIAnimator { + + public init?( + durationSeconds: CFTimeInterval, + sizeStart: CGSize, + sizeEnd: CGSize, + onSizeDidChange: ((_ newSize: CGSize) -> Void)? = nil, + onAnimationCompletion: (() -> Void)? = nil + ) { + super.init( + durationSeconds: durationSeconds, + animatedValuesStart: sizeStart.array, + animatedValuesEnd: sizeEnd.array + ) { + onSizeDidChange?( + CGSize(array: $0) + ); + } onAnimationCompletion: { + onAnimationCompletion?(); + }; + }; + + public func update( + sizeEnd: CGSize, + duration: CGFloat? = nil + ){ + super.update( + animatedValuesEnd: sizeEnd.array, + duration: duration + ); + }; +}; diff --git a/ios/React Native/RNIComputable/RNIComputableOffset.swift b/ios/React Native/RNIComputable/RNIComputableOffset.swift new file mode 100644 index 00000000..04ab8f72 --- /dev/null +++ b/ios/React Native/RNIComputable/RNIComputableOffset.swift @@ -0,0 +1,61 @@ +// +// RNIComputableOffset.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/28/23. +// + +import UIKit + +public struct RNIComputableOffset { + + public enum OffsetOperation: String { + case multiply, divide, add, subtract; + + func compute(a: Double, b: Double) -> Double { + switch self { + case .add: + return a + b; + + case .divide: + return a / b; + + case .multiply: + return a * b; + + case .subtract: + return a - b; + }; + }; + }; + + public var offset: Double; + public var offsetOperation: OffsetOperation; + + public func compute( + withValue value: Double, + isValueOnRHS: Bool = false + ) -> Double { + if isValueOnRHS { + return self.offsetOperation.compute(a: value, b: self.offset); + }; + + return self.offsetOperation.compute(a: self.offset, b: value); + }; +}; + +extension RNIComputableOffset { + + public init?(fromDict dict: NSDictionary){ + guard let offset = dict["offset"] as? NSNumber else { return nil }; + self.offset = offset.doubleValue; + + self.offsetOperation = { + guard let offsetOperationRaw = dict["offsetOperation"] as? String, + let offsetOperation = OffsetOperation(rawValue: offsetOperationRaw) + else { return .add }; + + return offsetOperation; + }(); + }; +}; diff --git a/ios/React Native/RNIComputable/RNIComputableSize.swift b/ios/React Native/RNIComputable/RNIComputableSize.swift new file mode 100644 index 00000000..5209adcf --- /dev/null +++ b/ios/React Native/RNIComputable/RNIComputableSize.swift @@ -0,0 +1,148 @@ +// +// RNIComputableValue.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/26/23. +// + +import UIKit + +public struct RNIComputableSize { + + // MARK: - Properties + // ------------------ + + public let mode: RNIComputableSizeMode; + + public let offsetWidth: RNIComputableOffset?; + public let offsetHeight: RNIComputableOffset?; + + public let minWidth: CGFloat?; + public let minHeight: CGFloat?; + + public let maxWidth: CGFloat?; + public let maxHeight: CGFloat?; + + // MARK: - Internal Functions + // -------------------------- + + func sizeWithOffsets(forSize size: CGSize) -> CGSize { + let offsetWidth = + self.offsetWidth?.compute(withValue: size.width); + + let offsetHeight = + self.offsetHeight?.compute(withValue: size.height); + + return CGSize( + width: offsetWidth ?? size.width, + height: offsetHeight ?? size.height + ); + }; + + func sizeWithClamp(forSize size: CGSize) -> CGSize { + return CGSize( + width: size.width.clamped( + min: self.minWidth, + max: self.maxWidth + ), + height: size.height.clamped( + min: self.minHeight, + max: self.maxHeight + ) + ); + }; + + // MARK: - Functions + // ----------------- + + public func computeRaw( + withTargetSize targetSize: CGSize, + currentSize: CGSize + ) -> CGSize { + switch self.mode { + case .current: + return currentSize; + + case .stretch: + return targetSize; + + case let .constant(constantWidth, constantHeight): + return CGSize(width: constantWidth, height: constantHeight); + + case let .percent(percentWidth, percentHeight): + return CGSize( + width: percentWidth * targetSize.width, + height: percentHeight * targetSize.height + ); + }; + }; + + public func compute( + withTargetSize targetSize: CGSize, + currentSize: CGSize + ) -> CGSize { + let rawSize = self.computeRaw( + withTargetSize: targetSize, + currentSize: currentSize + ); + + let clampedSize = self.sizeWithClamp(forSize: rawSize); + return self.sizeWithOffsets(forSize: clampedSize); + }; +}; + +extension RNIComputableSize { + public init?(fromDict dict: NSDictionary){ + guard let mode = RNIComputableSizeMode(fromDict: dict) + else { return nil }; + + self.mode = mode; + + self.offsetWidth = { + guard let offsetRaw = dict["offsetWidth"] as? NSDictionary, + let offset = RNIComputableOffset(fromDict: offsetRaw) + else { return nil }; + + return offset; + }(); + + self.offsetHeight = { + guard let offsetRaw = dict["offsetHeight"] as? NSDictionary, + let offset = RNIComputableOffset(fromDict: offsetRaw) + else { return nil }; + + return offset; + }(); + + self.minWidth = + Self.getDoubleValue(forDict: dict, withKey: "minWidth"); + + self.minHeight = + Self.getDoubleValue(forDict: dict, withKey: "minHeight"); + + self.maxWidth = + Self.getDoubleValue(forDict: dict, withKey: "maxWidth"); + + self.maxHeight = + Self.getDoubleValue(forDict: dict, withKey: "maxHeight"); + }; + + public init(mode: RNIComputableSizeMode){ + self.mode = mode; + + self.offsetWidth = nil; + self.offsetHeight = nil; + self.minWidth = nil; + self.minHeight = nil; + self.maxWidth = nil; + self.maxHeight = nil; + }; + + static private func getDoubleValue( + forDict dict: NSDictionary, + withKey key: String + ) -> CGFloat? { + guard let number = dict[key] as? NSNumber else { return nil }; + return number.doubleValue; + }; +}; diff --git a/ios/React Native/RNIComputable/RNIComputableSizeMode.swift b/ios/React Native/RNIComputable/RNIComputableSizeMode.swift new file mode 100644 index 00000000..d2c751fb --- /dev/null +++ b/ios/React Native/RNIComputable/RNIComputableSizeMode.swift @@ -0,0 +1,60 @@ +// +// RNIComputableSizeOffset.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/28/23. +// + +import UIKit + +public enum RNIComputableSizeMode { + case current; + case stretch; + + case constant( + constantWidth: Double, + constantHeight: Double + ); + + case percent( + percentWidth: Double, + percentHeight: Double + ); +}; + +extension RNIComputableSizeMode { + public init?(fromDict dict: NSDictionary){ + guard let mode = dict["mode"] as? String else { return nil }; + + switch mode { + case "current": + self = .current; + + case "stretch": + self = .stretch; + + case "constant": + guard let width = dict["constantWidth"] as? NSNumber, + let height = dict["constantHeight"] as? NSNumber + else { return nil }; + + self = .constant( + constantWidth: width.doubleValue, + constantHeight: height.doubleValue + ); + + case "percent": + guard let width = dict["percentWidth"] as? NSNumber, + let height = dict["percentHeight"] as? NSNumber + else { return nil }; + + self = .percent( + percentWidth: width.doubleValue, + percentHeight: height.doubleValue + ); + + default: + return nil; + }; + }; +}; diff --git a/ios/React Native/RNIComputable/RNIComputableValue.swift b/ios/React Native/RNIComputable/RNIComputableValue.swift new file mode 100644 index 00000000..9672b19d --- /dev/null +++ b/ios/React Native/RNIComputable/RNIComputableValue.swift @@ -0,0 +1,116 @@ +// +// RNIComputableValue.swift +// swift-programmatic-modal +// +// Created by Dominic Go on 5/19/23. +// + +import UIKit + +public struct RNIComputableValue { + + // MARK: - Properties + // ------------------ + + public let mode: RNIComputableValueMode; + + public let offset: RNIComputableOffset?; + + public let minValue: CGFloat?; + public let maxValue: CGFloat?; + + + // MARK: - Internal Functions + // -------------------------- + + func valueWithOffsets(forValue value: CGFloat) -> CGFloat { + return self.offset?.compute(withValue: value) ?? value; + }; + + func valueWithClamp(forValue value: CGFloat) -> CGFloat { + return value.clamped( + min: self.minValue, + max: self.maxValue + ); + }; + + // MARK: - Functions + // ----------------- + + public func computeRaw( + withTargetValue targetValue: CGFloat, + currentValue: CGFloat + ) -> CGFloat { + switch self.mode { + case .current: + return currentValue; + + case .stretch: + return targetValue; + + case let .constant(constantValue): + return constantValue; + + case let .percent(percentValue): + return percentValue * targetValue; + }; + }; + + public func compute( + withTargetValue targetValue: CGFloat, + currentValue: CGFloat + ) -> CGFloat { + let rawValue = self.computeRaw( + withTargetValue: targetValue, + currentValue: currentValue + ); + + let clampedValue = self.valueWithClamp(forValue: rawValue); + return self.valueWithOffsets(forValue: clampedValue); + }; + + public init( + mode: RNIComputableValueMode, + offset: RNIComputableOffset? = nil, + minValue: CGFloat? = nil, + maxValue: CGFloat? = nil + ) { + self.mode = mode; + self.offset = offset; + self.minValue = minValue; + self.maxValue = maxValue; + }; +}; + +extension RNIComputableValue { + public init?(fromDict dict: NSDictionary){ + guard let mode = RNIComputableValueMode(fromDict: dict) + else { return nil }; + + self.mode = mode; + + self.offset = { + guard let offsetRaw = dict["offset"] as? NSDictionary, + let offset = RNIComputableOffset(fromDict: offsetRaw) + else { return nil }; + + return offset; + }(); + + self.minValue = + Self.getDoubleValue(forDict: dict, withKey: "minValue"); + + self.maxValue = + Self.getDoubleValue(forDict: dict, withKey: "maxValue"); + }; + + + + static private func getDoubleValue( + forDict dict: NSDictionary, + withKey key: String + ) -> CGFloat? { + guard let number = dict[key] as? NSNumber else { return nil }; + return number.doubleValue; + }; +}; diff --git a/ios/React Native/RNIComputable/RNIComputableValueMode.swift b/ios/React Native/RNIComputable/RNIComputableValueMode.swift new file mode 100644 index 00000000..3e75f8e6 --- /dev/null +++ b/ios/React Native/RNIComputable/RNIComputableValueMode.swift @@ -0,0 +1,48 @@ +// +// RNIComputableValueMode.swift +// swift-programmatic-modal +// +// Created by Dominic Go on 5/19/23. +// + +import UIKit + + +public enum RNIComputableValueMode { + case current; + case stretch; + case constant(constantValue: Double); + case percent(percentValue: Double); +}; + +extension RNIComputableValueMode { + public init?(fromDict dict: NSDictionary){ + guard let mode = dict["mode"] as? String else { return nil }; + + switch mode { + case "current": + self = .current; + + case "stretch": + self = .stretch; + + case "constant": + guard let value = dict["constantValue"] as? NSNumber + else { return nil }; + + self = .constant( + constantValue: value.doubleValue + ); + + case "percent": + guard let value = dict["percentValue"] as? NSNumber + else { return nil }; + + self = .percent(percentValue: value.doubleValue); + + default: + return nil; + }; + }; +}; + diff --git a/ios/React Native/RNIComputable/RNIViewMetadata.swift b/ios/React Native/RNIComputable/RNIViewMetadata.swift new file mode 100644 index 00000000..a8851122 --- /dev/null +++ b/ios/React Native/RNIComputable/RNIViewMetadata.swift @@ -0,0 +1,62 @@ +// +// RNIViewMetadata.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + + +public final class RNIViewMetadata: RNIDictionarySynthesizable { + + public let tag: Int; + + public let reactTag: NSNumber?; + public let nativeID: String?; + + public let parentView: RNIViewMetadata?; + public let subviews: [RNIViewMetadata]?; + + public required init( + fromView view: UIView, + setParentView: Bool = true, + setSubViews: Bool = true + ){ + self.tag = view.tag; + + self.reactTag = { + guard let reactTag = view.reactTag else { return nil }; + return reactTag; + }(); + + self.nativeID = { + guard let nativeID = view.nativeID else { return nil }; + return nativeID; + }(); + + self.parentView = { + guard setParentView, + let parentView = view.superview + else { return nil }; + + return Self.init( + fromView: parentView, + setParentView: false, + setSubViews: false + ); + }(); + + self.subviews = { + guard setSubViews else { return nil }; + + return view.subviews.map { + return Self.init( + fromView: $0, + setParentView: false, + setSubViews: false + ); + }; + }(); + }; +}; diff --git a/ios/React Native/RNIDictionarySynthesizable/RNIDictionaryRepresentable.swift b/ios/React Native/RNIDictionarySynthesizable/RNIDictionaryRepresentable.swift new file mode 100644 index 00000000..0bb912a4 --- /dev/null +++ b/ios/React Native/RNIDictionarySynthesizable/RNIDictionaryRepresentable.swift @@ -0,0 +1,12 @@ +// +// RNIDictionaryRepresentable.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/3/23. +// + +import UIKit + +public protocol RNIDictionaryRepresentable { + var asDictionary: [String: Any] { get }; +}; diff --git a/ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable+Default.swift b/ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable+Default.swift new file mode 100644 index 00000000..acaec5dd --- /dev/null +++ b/ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable+Default.swift @@ -0,0 +1,140 @@ +// +// RNIDictionarySynthesizable+Default.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +extension RNIDictionarySynthesizable { + + // MARK: - Static Properties + // ------------------------- + + public static var synthesizedDictionaryIgnore: [String] { + []; + }; + + public static var synthesizedDictionaryInlinedProperties: [PartialKeyPath] { + []; + }; + + // MARK: - Static Functions + // ------------------------ + + fileprivate static func recursivelyParseValue( + _ value: Any, + isJSDict: Bool + ) -> Any { + + if let synthesizableDict = value as? (any RNIDictionarySynthesizable) { + return synthesizableDict.synthesizedDictionary(isJSDict: isJSDict); + }; + + if isJSDict, let rawValue = value as? any RawRepresentable { + return rawValue.rawValue; + }; + + if isJSDict, let array = value as? Array { + return array.map { + return Self.recursivelyParseValue($0, isJSDict: isJSDict); + }; + }; + + if isJSDict, let dict = value as? Dictionary { + return dict.mapValues { + return Self.recursivelyParseValue($0, isJSDict: isJSDict); + }; + }; + + if let dictRepresentable = value as? RNIDictionaryRepresentable { + let dict = dictRepresentable.asDictionary; + + guard isJSDict else { return dict }; + return Self.recursivelyParseValue(dict, isJSDict: isJSDict); + }; + + if let encodable = value as? Encodable, + let dict = encodable.asDictionary { + + return dict; + }; + + return value; + }; + + private func mergeInlinedProperties( + withDict baseDict: Dictionary, + isJSDict: Bool + ) -> Dictionary { + + var baseDict = baseDict; + + Self.synthesizedDictionaryInlinedProperties.forEach { + guard let value = self[keyPath: $0] as? (any RNIDictionarySynthesizable) + else { return }; + + let inlinedDict = value.synthesizedDictionary(isJSDict: isJSDict); + baseDict = baseDict.merging(inlinedDict){ old, _ in old }; + }; + + return baseDict; + }; + + private func synthesizedDictionaryUsingDictIgnore( + isJSDict: Bool + ) -> Dictionary { + + let mirror = Mirror(reflecting: self); + let properties = mirror.children; + + #if DEBUG + /// Runtime Check - Verify if `synthesizedDictionaryIgnore` is valid + for propertyKeyToIgnore in Self.synthesizedDictionaryIgnore { + if !properties.contains(where: { $0.label == propertyKeyToIgnore }) { + fatalError( + "Invalid value of '\(propertyKeyToIgnore)' in " + + "'synthesizedDictionaryIgnore' for '\(Self.self)' - " + + "No property named '\(propertyKeyToIgnore)' in '\(Self.self)'" + ); + }; + }; + #endif + + let propertyValueMap = properties.lazy.map { ( + propertyKey: String?, value: Any) -> (String, Any)? in + + guard let propertyKey = propertyKey, + !Self.synthesizedDictionaryIgnore.contains(propertyKey) + else { return nil }; + + let parsedValue = Self.recursivelyParseValue(value, isJSDict: isJSDict); + return (propertyKey, parsedValue); + }; + + let baseDict = Dictionary( + uniqueKeysWithValues: propertyValueMap.compactMap { $0 } + ); + + return self.mergeInlinedProperties( + withDict: baseDict, + isJSDict: isJSDict + ); + }; + + // MARK: - Public Functions + // ------------------------ + + public func synthesizedDictionary( + isJSDict: Bool + ) -> Dictionary { + + return self.synthesizedDictionaryUsingDictIgnore(isJSDict: isJSDict); + }; + + + public var synthesizedJSDictionary: Dictionary { + self.synthesizedDictionary(isJSDict: true); + }; +}; diff --git a/ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable.swift b/ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable.swift new file mode 100644 index 00000000..3deda88e --- /dev/null +++ b/ios/React Native/RNIDictionarySynthesizable/RNIDictionarySynthesizable.swift @@ -0,0 +1,29 @@ +// +// RNIDictionarySynthesizable.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/26/23. +// + +import UIKit + +/// Any type that conforms to this protocol will be able to create a dictionary +/// representing the keys and values inside that type +/// +public protocol RNIDictionarySynthesizable { + + /// The names/identifiers of the property to be ignored when + /// `synthesizedDictionary` is created. + /// + static var synthesizedDictionaryIgnore: [String] { get }; + + /// The key path to the property that will be inlined/"squashed together" + /// into `synthesizedDictionary`. + /// + static var synthesizedDictionaryInlinedProperties: + [PartialKeyPath] { get }; + + /// A map of the property names and their respective values + func synthesizedDictionary(isJSDict: Bool) -> Dictionary; +}; + diff --git a/ios/React Native/RNIError/RNIBaseError+Helpers.swift b/ios/React Native/RNIError/RNIBaseError+Helpers.swift new file mode 100644 index 00000000..2d0d2691 --- /dev/null +++ b/ios/React Native/RNIError/RNIBaseError+Helpers.swift @@ -0,0 +1,120 @@ +// +// RNIBaseError+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/12/23. +// + +import UIKit +import React + +extension RNIBaseError { + + public var errorMessage: String { + var message = "message: \(self.message ?? "N/A")"; + + #if DEBUG + message += " - debugMessage: \(self.debugMessage ?? "N/A")"; + #endif + + if let fileID = self.fileID { + message += "- fileID: \(fileID)"; + }; + + if let functionName = self.functionName { + message += "- functionName: \(functionName)"; + }; + + if let lineNumber = self.lineNumber { + message += "- lineNumber: \(lineNumber)"; + }; + + return message; + }; + + public init( + code: ErrorCode, + message: String? = nil, + debugMessage: String? = nil, + debugData: Dictionary? = nil, + fileID: String? = #fileID, + functionName: String? = #function, + lineNumber: Int? = #line + ) { + self.init( + code: code, + message: message, + debugMessage: debugMessage + ); + + self.fileID = fileID; + self.functionName = functionName; + self.lineNumber = lineNumber; + }; + + public init( + code: ErrorCode, + error: Error, + debugMessage: String? = nil, + debugData: Dictionary? = nil, + fileID: String? = #fileID, + functionName: String? = #function, + lineNumber: Int? = #line + ) { + self.init( + code: code, + message: error.localizedDescription, + debugMessage: debugMessage + ); + + self.fileID = fileID; + self.functionName = functionName; + self.lineNumber = lineNumber; + }; + + public mutating func setDebugValues( + fileID: String = #fileID, + functionName: String = #function, + lineNumber: Int = #line + ) { + self.fileID = fileID; + self.functionName = functionName; + self.lineNumber = lineNumber; + }; + + public mutating func addDebugData(_ nextDebugData: Dictionary){ + guard let prevDebugData = self.debugData else { + self.debugData = nextDebugData; + return; + }; + + self.debugData = prevDebugData.merging(nextDebugData) { (_, new) in new }; + }; +}; + +extension RNIBaseError where Self: RNIDictionarySynthesizable { + + public var asNSError: NSError? { + + let errorCode = self.code.errorCode ?? + RNIGenericErrorCode.unspecified.errorCode; + + guard let errorCode = errorCode else { return nil }; + + return NSError( + domain: Self.domain, + code: errorCode, + userInfo: self.synthesizedJSDictionary + ); + }; + + public func invokePromiseRejectBlock( + _ block: @escaping RCTPromiseRejectBlock + ) { + block( + /* code */ self.code.description, + /* message */ self.errorMessage, + /* error */ self.asNSError + ); + }; +}; diff --git a/ios/React Native/RNIError/RNIBaseError.swift b/ios/React Native/RNIError/RNIBaseError.swift new file mode 100644 index 00000000..b19a9770 --- /dev/null +++ b/ios/React Native/RNIError/RNIBaseError.swift @@ -0,0 +1,34 @@ +// +// RNINavigatorError.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 9/11/21. +// + +import UIKit + +/// TODO - Move to `react-native-ios-utilities` +/// * Replace older impl. of `RNIError` with this version + +public protocol RNIBaseError: Error { + + associatedtype ErrorCode: RNIErrorCode; + + static var domain: String { get }; + + var code: ErrorCode { get }; + var message: String? { get }; + + var debugMessage: String? { get }; + var debugData: Dictionary? { get set } + + var fileID : String? { get set }; + var functionName: String? { get set }; + var lineNumber : Int? { get set }; + + init( + code: ErrorCode, + message: String?, + debugMessage: String? + ); +}; diff --git a/ios/React Native/RNIError/RNIError.swift b/ios/React Native/RNIError/RNIError.swift new file mode 100644 index 00000000..0d96e5d1 --- /dev/null +++ b/ios/React Native/RNIError/RNIError.swift @@ -0,0 +1,18 @@ +// +// RNIError.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/12/23. +// + +import UIKit + +public typealias RNIError = + RNIBaseError & RNIDictionarySynthesizable; + +public typealias RNIErrorCode = + CustomStringConvertible + & CaseIterable + & Equatable + & RNIErrorCodeDefaultable + & RNIErrorCodeSynthesizable; diff --git a/ios/React Native/RNIError/RNIErrorCodeDefaultable.swift b/ios/React Native/RNIError/RNIErrorCodeDefaultable.swift new file mode 100644 index 00000000..9d5a0310 --- /dev/null +++ b/ios/React Native/RNIError/RNIErrorCodeDefaultable.swift @@ -0,0 +1,70 @@ +// +// RNIGenericErrorDefaultable.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 8/28/22. +// + +import UIKit + +public protocol RNIErrorCodeDefaultable { + + static var runtimeError : Self { get }; + static var libraryError : Self { get }; + static var reactError : Self { get }; + static var unknownError : Self { get }; + static var invalidArgument: Self { get }; + static var outOfBounds : Self { get }; + static var invalidReactTag: Self { get }; + static var nilValue : Self { get }; +}; + +// MARK: - Default +// --------------- + +public extension RNIErrorCodeDefaultable { + + static var runtimeError: Self { + Self.runtimeError + }; + + static var libraryError: Self { + Self.libraryError + }; + + static var reactError: Self { + Self.reactError + }; + + static var unknownError: Self { + Self.unknownError + }; + + static var invalidArgument: Self { + Self.invalidArgument + }; + + static var outOfBounds: Self { + Self.outOfBounds + }; + + static var invalidReactTag: Self { + Self.invalidReactTag + }; + + static var nilValue: Self { + Self.nilValue + }; +}; + +// MARK: - Helpers +// --------------- + +extension RNIErrorCodeDefaultable where Self: RawRepresentable { + + public var description: String { + self.rawValue; + }; +}; + + diff --git a/ios/React Native/RNIError/RNIErrorCodeSynthesizable.swift b/ios/React Native/RNIError/RNIErrorCodeSynthesizable.swift new file mode 100644 index 00000000..c71efffb --- /dev/null +++ b/ios/React Native/RNIError/RNIErrorCodeSynthesizable.swift @@ -0,0 +1,24 @@ +// +// RNIErrorCodeSynthesizable.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/12/23. +// + +import UIKit + +public protocol RNIErrorCodeSynthesizable { + var errorCode: Int? { get }; +} + +extension RNIErrorCodeSynthesizable where Self: CaseIterable & Equatable { + + public var errorCode: Int? { + let match = Self.allCases.enumerated().first { _, value in + value == self + } + + guard let offset = match?.offset else { return nil }; + return offset * -1; + }; +}; diff --git a/ios/React Native/RNIError/RNIGenericErrorCode.swift b/ios/React Native/RNIError/RNIGenericErrorCode.swift new file mode 100644 index 00000000..9689fe14 --- /dev/null +++ b/ios/React Native/RNIError/RNIGenericErrorCode.swift @@ -0,0 +1,15 @@ +// +// RNIGenericErrorCode.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/12/23. +// + +import UIKit + + +enum RNIGenericErrorCode: + String, CaseIterable, RNIErrorCodeDefaultable, RNIErrorCodeSynthesizable { + + case unspecified; +}; diff --git a/ios/React Native/RNIIdentifiable/RNIIdentifiable+Default.swift b/ios/React Native/RNIIdentifiable/RNIIdentifiable+Default.swift new file mode 100644 index 00000000..6b25ec6d --- /dev/null +++ b/ios/React Native/RNIIdentifiable/RNIIdentifiable+Default.swift @@ -0,0 +1,37 @@ +// +// RNIIdentifiable+Default.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +extension RNIIdentifiable { + public static var synthesizedIdPrefix: String { + String(describing: Self.self) + "-"; + }; + + public var synthesizedIdentifier: RNIObjectIdentifier { + if let identifier = self.metadata { + return identifier; + }; + + let identifier = RNIObjectIdentifier(type: Self.self); + self.metadata = identifier; + + return identifier; + }; + + public var synthesizedID: Int { + self.synthesizedIdentifier.id; + }; + + public var synthesizedStringID: String { + Self.synthesizedIdPrefix + "\(self.synthesizedID)"; + }; + + public var synthesizedUUID: UUID { + self.synthesizedIdentifier.uuid; + }; +}; diff --git a/ios/React Native/RNIIdentifiable/RNIIdentifiable.swift b/ios/React Native/RNIIdentifiable/RNIIdentifiable.swift new file mode 100644 index 00000000..e70d473d --- /dev/null +++ b/ios/React Native/RNIIdentifiable/RNIIdentifiable.swift @@ -0,0 +1,20 @@ +// +// RNIIdentifiable.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/31/23. +// + +import UIKit + +public protocol RNIIdentifiable: + AnyObject, RNIObjectMetadata where T == RNIObjectIdentifier { + + static var synthesizedIdPrefix: String { get }; + + var synthesizedID: Int { get }; + + var synthesizedUUID: UUID { get }; + +}; + diff --git a/ios/React Native/RNIIdentifiable/RNIObjectIdentifier.swift b/ios/React Native/RNIIdentifiable/RNIObjectIdentifier.swift new file mode 100644 index 00000000..ed9c839a --- /dev/null +++ b/ios/React Native/RNIIdentifiable/RNIObjectIdentifier.swift @@ -0,0 +1,54 @@ +// +// RNIObjectIdentifier.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +fileprivate class Counter { + static var typeToCounterMap: Dictionary = [:]; + + static func getTypeString(ofType _type: Any) -> String { + return String(describing: type(of: _type)); + }; + + static func set(forType type: Any, counter: Int) { + let typeString = Self.getTypeString(ofType: type); + Self.typeToCounterMap[typeString] = counter; + }; + + static func set(forType typeString: String, counter: Int) { + Self.typeToCounterMap[typeString] = counter; + }; + + static func get(forType type: Any) -> Int { + let typeString = Self.getTypeString(ofType: type); + + guard let counter = Self.typeToCounterMap[typeString] else { + Self.set(forType: typeString, counter: -1); + return -1; + }; + + return counter; + }; + + static func getAndIncrement(forType type: Any) -> Int { + let prevCount = Self.get(forType: type); + let nextCount = prevCount + 1; + + Self.set(forType: type, counter: nextCount); + return nextCount; + }; +}; + +public final class RNIObjectIdentifier { + + public let id: Int; + public let uuid = UUID(); + + public init(type: Any) { + self.id = Counter.getAndIncrement(forType: type); + }; +}; diff --git a/ios/React Native/RNIModal/RNIModal+Helpers.swift b/ios/React Native/RNIModal/RNIModal+Helpers.swift new file mode 100644 index 00000000..621a8979 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModal+Helpers.swift @@ -0,0 +1,122 @@ +// +// RNIModal+Helpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/18/23. +// + +import UIKit + +extension RNIModalIdentifiable where Self: RNIIdentifiable { + public var modalNativeID: String { + self.synthesizedStringID + }; +}; + +extension RNIModalIdentifiable { + public var modalUserID: String? { + nil + }; +}; + +extension RNIModalState where Self: RNIModalPresentation { + + internal var synthesizedWindowMapData: RNIWindowMapData? { + guard let window = self.window else { return nil }; + return RNIModalWindowMapShared.get(forWindow: window); + }; + + /// Programmatically check if this instance is presented + public var computedIsModalPresented: Bool { + let listPresentedVC = + RNIPresentedVCListCache.getPresentedViewControllers(forWindow: window); + + return listPresentedVC.contains { + $0 === self.modalViewController; + }; + }; + + /// Programmatically check if this instance is in focus + public var computedIsModalInFocus: Bool { + let listPresentedVC = + RNIPresentedVCListCache.getPresentedViewControllers(forWindow: window); + + guard let topmostVC = listPresentedVC.last + else { return self.synthesizedIsModalInFocus }; + + return topmostVC === self.modalViewController; + }; + + /// Note:2023-03-31-15-41-04 + /// + /// * This is based on the view controller hierarchy + /// * So parent/child view controller that aren't modals are also counted + /// + public var computedViewControllerIndex: Int { + let listPresentedVC = + RNIPresentedVCListCache.getPresentedViewControllers(forWindow: window); + + for (index, vc) in listPresentedVC.enumerated() { + guard vc === self.modalViewController else { continue }; + return index; + }; + + return -1; + }; + + /// Programmatically get the "modal index" + public var computedModalIndex: Int { + guard let window = self.window, + let modalVC = self.modalViewController + else { return -1 }; + + return RNIModalUtilities.computeModalIndex( + forWindow: window, + forViewController: modalVC + ); + }; + + public var currentModalIndex: Int { + self.synthesizedWindowMapData?.modalIndexCurrent ?? -1; + }; + + public var computedCurrentModalIndex: Int { + RNIModalUtilities.computeModalIndex(forWindow: self.window); + }; + + public var synthesizedIsModalInFocus: Bool { + self.modalPresentationState.isPresented && + self.modalFocusState.state.isFocused; + }; +}; + +extension RNIModalState where Self: RNIModal { + + public var synthesizedModalData: RNIModalData { + let purgeCache = + RNIPresentedVCListCache.beginCaching(forWindow: self.window); + + let modalData = RNIModalData( + modalNativeID: self.modalNativeID, + + modalIndex: self.modalIndex, + modalIndexPrev: self.modalIndexPrev, + currentModalIndex: self.currentModalIndex, + + modalFocusState: self.modalFocusState, + modalPresentationState: self.modalPresentationState, + + computedIsModalInFocus: self.computedIsModalInFocus, + computedIsModalPresented: self.computedIsModalPresented, + + computedModalIndex: self.computedModalIndex, + computedViewControllerIndex: self.computedViewControllerIndex, + computedCurrentModalIndex: self.computedCurrentModalIndex, + + synthesizedWindowID: self.window?.synthesizedStringID + ); + + purgeCache(); + return modalData; + }; +}; diff --git a/ios/React Native/RNIModal/RNIModal.swift b/ios/React Native/RNIModal/RNIModal.swift new file mode 100644 index 00000000..495034aa --- /dev/null +++ b/ios/React Native/RNIModal/RNIModal.swift @@ -0,0 +1,125 @@ +// +// RNIModal.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/16/23. +// + +import UIKit + +/// A collection of protocols that the "adoptee" needs to implement in order to +/// be considered a "modal". +/// +public typealias RNIModal = + RNIIdentifiable + & RNIModalIdentifiable + & RNIModalState + & RNIModalRequestable + & RNIModalFocusNotifiable + & RNIModalPresentationNotifying + & RNIModalPresentation; + + +/// Contains modal-related properties for identifying a specific modal instance +/// +/// Specifies that the "adoptee/delegate" that conforms to this protocol must +/// have the specified modal-related properties so that it can be uniquely +/// identified amongst different modal instances. +/// +public protocol RNIModalIdentifiable: AnyObject { + + var modalNativeID: String { get }; + + var modalUserID: String? { get }; +}; + +/// Contains modal-related properties for keeping track of the state of the +/// modal. +/// +/// Specifies that the "adoptee/delegate" that conforms to this protocol must +/// have the specified modal-related properties for keeping track of state +/// +/// The "implementor/delegator" updates these properties; The delegate +/// should treat the properties declared in this protocol as read-only. +/// +public protocol RNIModalState: AnyObject { + + var modalIndexPrev: Int! { set get }; + + var modalIndex: Int! { set get }; + + var modalPresentationState: RNIModalPresentationStateMachine { set get }; + + var modalFocusState: RNIModalFocusStateMachine { set get }; +}; + +/// Contains functions that are invoked to request modal-related actions. +/// +/// The "implementer/delegator" notifies the "adoptee/delegate" of this protocol +/// of requests to perform "modal-related" actions. +/// +public protocol RNIModalRequestable: AnyObject { + + func requestModalToShow( + sender: Any, + animated: Bool, + completion: @escaping () -> Void + ) throws; + + func requestModalToHide( + sender: Any, + animated: Bool, + completion: @escaping () -> Void + ) throws; +}; + +/// Contains functions that get called whenever a modal-related event occurs. +/// +/// The "implementer/delegator" notifies the "adoptee/delegate" of this protocol +/// of modal focus/blur related events. +/// +/// An interface for the "adoptee/delegate" to receive and handle incoming +/// modal "focus/blur"-related notifications. +/// +public protocol RNIModalFocusNotifiable: AnyObject { + func onModalWillFocusNotification(sender: any RNIModal); + + func onModalDidFocusNotification(sender: any RNIModal); + + func onModalWillBlurNotification(sender: any RNIModal); + + func onModalDidBlurNotification(sender: any RNIModal); +}; + +/// Specifies that the "adoptee/delegate" that conforms to this protocol must +/// notify a delegate of modal presentation-related events +/// +public protocol RNIModalPresentationNotifying: AnyObject { + + /// Notify the "shared modal manager" if the current modal instance is going + /// to be shown or hidden. + /// + /// That focus notification will then be relayed to the other modal instances. + /// + var modalPresentationNotificationDelegate: + RNIModalPresentationNotifiable! { get set }; +}; + +/// Properties related to modal presentation. +/// +/// Specifies that the "adoptee/delegate" that conforms to this protocol must +/// implement this so that the "delegator" understands how the delegate presents +/// the modal. +/// +public protocol RNIModalPresentation: AnyObject { + + /// Returns the modal view controller that is to be presented + var modalViewController: UIViewController? { get }; + + /// Returns the view controller that presented the `modalViewController` + /// instance. + var presentingViewController: UIViewController? { get }; + + /// The "main" window for this instance + var window: UIWindow? { get }; +}; diff --git a/ios/React Native/RNIModal/RNIModalCustomSheetDetent.swift b/ios/React Native/RNIModal/RNIModalCustomSheetDetent.swift new file mode 100644 index 00000000..be772298 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalCustomSheetDetent.swift @@ -0,0 +1,111 @@ +// +// RNIModalCustomSheetDetent.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/21/23. +// + +import UIKit + +enum RNIModalCustomSheetDetentMode { + + case relative(value: Double); + case constant(value: Double); + + var rawValueString: String { + switch self { + case .constant: return "constant"; + case .relative: return "relative"; + }; + }; +}; + +struct RNIModalCustomSheetDetent { + + typealias OnDetentDidCreate = ( + _ containerTraitCollection: UITraitCollection, + _ maximumDetentValue: CGFloat, + _ computedDetentValue: CGFloat, + _ sender: RNIModalCustomSheetDetent + ) -> Void; + + let mode: RNIModalCustomSheetDetentMode; + let key: String; + + let offset: RNIComputableOffset?; + + let onDetentDidCreate: OnDetentDidCreate?; + + init?( + forDict dict: Dictionary, + onDetentDidCreate: OnDetentDidCreate? = nil + ) { + guard let rawMode = dict["mode"] as? String, + let rawKey = dict["key"] as? String + else { return nil }; + + self.key = rawKey; + self.onDetentDidCreate = onDetentDidCreate; + + let mode: RNIModalCustomSheetDetentMode? = { + switch rawMode { + case "relative": + guard let rawValue = dict["sizeMultiplier"] as? NSNumber + else { return nil }; + + return .relative(value: rawValue.doubleValue); + + case "constant": + guard let rawValue = dict["sizeConstant"] as? NSNumber + else { return nil }; + + return .constant(value: rawValue.doubleValue); + + default: + return nil; + }; + }(); + + guard let mode = mode else { return nil }; + self.mode = mode; + + self.offset = RNIComputableOffset(fromDict: dict as NSDictionary); + }; + + @available(iOS 15.0, *) + var synthesizedDetentIdentifier: + UISheetPresentationController.Detent.Identifier { + + UISheetPresentationController.Detent.Identifier(self.key); + }; + + @available(iOS 16.0, *) + var synthesizedDetent: UISheetPresentationController.Detent { + return .custom(identifier: self.synthesizedDetentIdentifier) { context in + + let computedValueBase: Double = { + switch self.mode { + case let .relative(value): + return value * context.maximumDetentValue; + + case let .constant(value): + return value; + }; + }(); + + let computedValueWithOffset = + self.offset?.compute(withValue: computedValueBase); + + let computedValue = computedValueWithOffset ?? computedValueBase; + + self.onDetentDidCreate?( + context.containerTraitCollection, + context.maximumDetentValue, + computedValue, + self + ); + + return computedValue; + }; + }; +}; diff --git a/ios/React Native/RNIModal/RNIModalData.swift b/ios/React Native/RNIModal/RNIModalData.swift new file mode 100644 index 00000000..d8ec76ae --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalData.swift @@ -0,0 +1,28 @@ +// +// RNIModalData.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/26/23. +// + +import UIKit + +public struct RNIModalData: RNIDictionarySynthesizable { + public let modalNativeID: String; + + public let modalIndex: Int; + public let modalIndexPrev: Int; + public let currentModalIndex: Int; + + public let modalFocusState: RNIModalFocusStateMachine; + public let modalPresentationState: RNIModalPresentationStateMachine; + + public let computedIsModalInFocus: Bool; + public let computedIsModalPresented: Bool; + + public let computedModalIndex: Int; + public let computedViewControllerIndex: Int; + public let computedCurrentModalIndex: Int; + + public let synthesizedWindowID: String?; +}; diff --git a/ios/React Native/RNIModal/RNIModalError.swift b/ios/React Native/RNIModal/RNIModalError.swift new file mode 100644 index 00000000..3f5e2a85 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalError.swift @@ -0,0 +1,41 @@ +// +// RNIModalError.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/10/23. +// + +import UIKit + +public enum RNIModalErrorCode: String, RNIErrorCode { + case modalAlreadyVisible, + modalAlreadyHidden, + dismissRejected; +}; + +public struct RNIModalError: RNIError { + + public typealias ErrorCode = RNIModalErrorCode; + + public static var domain = "react-native-ios-modal"; + + public var code: RNIModalErrorCode; + public var message: String?; + + public var debugMessage: String?; + public var debugData: Dictionary?; + + public var fileID: String?; + public var functionName: String?; + public var lineNumber: Int?; + + public init( + code: ErrorCode, + message: String?, + debugMessage: String? + ) { + self.code = code; + self.message = message; + self.debugMessage = debugMessage; + }; +}; diff --git a/ios/React Native/RNIModal/RNIModalEventData.swift b/ios/React Native/RNIModal/RNIModalEventData.swift new file mode 100644 index 00000000..2d2b5189 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalEventData.swift @@ -0,0 +1,64 @@ +// +// RNIModalEvents.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/26/23. +// + +import UIKit + +public struct RNIModalBaseEventData: RNIDictionarySynthesizable { + + public static var synthesizedDictionaryIgnore: [String] { + ["modalData"] + }; + + public static var synthesizedDictionaryInlinedProperties: [ + PartialKeyPath + ] { + [\.modalData]; + }; + + public let reactTag: Int; + public let modalID: String?; + + public let modalData: RNIModalData; +}; + +public struct RNIOnModalFocusEventData: RNIDictionarySynthesizable { + + public static var synthesizedDictionaryIgnore: [String] { + ["modalData"] + }; + + public static var synthesizedDictionaryInlinedProperties: [ + PartialKeyPath + ] { + [\.modalData]; + }; + + public let modalData: RNIModalBaseEventData; + public let senderInfo: RNIModalData; + + public let isInitial: Bool; +}; + +public struct RNIModalDidChangeSelectedDetentIdentifierEventData: RNIDictionarySynthesizable { + public let sheetDetentStringPrevious: String?; + public let sheetDetentStringCurrent: String?; +}; + +public struct RNIModalDetentDidComputeEventData: RNIDictionarySynthesizable { + public let maximumDetentValue: CGFloat; + public let computedDetentValue: CGFloat; + public let key: String; +}; + +public struct RNIModalSwipeGestureEventData: RNIDictionarySynthesizable { + public let position: CGPoint; +}; + +public struct RNIModalDidSnapEventData: RNIDictionarySynthesizable { + public let selectedDetentIdentifier: String?; + public let modalContentSize: CGSize; +}; diff --git a/ios/React Native/RNIModal/RNIModalFlags.swift b/ios/React Native/RNIModal/RNIModalFlags.swift new file mode 100644 index 00000000..80491857 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalFlags.swift @@ -0,0 +1,21 @@ +// +// RNIModalFlags.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +public var RNIModalFlagsShared = RNIModalFlags.sharedInstance; + +public class RNIModalFlags { + static let sharedInstance = RNIModalFlags(); + + var isModalViewPresentationNotificationEnabled = true; + + var shouldSwizzleViewControllers = true; + var shouldSwizzledViewControllerNotifyAll = false; + var shouldWrapAllViewControllers = false; + +}; diff --git a/ios/React Native/RNIModal/RNIModalFocusState.swift b/ios/React Native/RNIModal/RNIModalFocusState.swift new file mode 100644 index 00000000..003df259 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalFocusState.swift @@ -0,0 +1,142 @@ +// +// RNIModalFocusState.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/8/23. +// + +import UIKit + + +public enum RNIModalFocusState: String { + case INITIAL; + + case FOCUSING + case FOCUSED; + + case BLURRING; + case BLURRED; + + // MARK: - Properties - Computed + // ----------------------------- + + public var isInitialized: Bool { + self != .INITIAL + }; + + public var isFocused: Bool { + self == .FOCUSED; + }; + + public var isBlurred: Bool { + self == .BLURRED || self == .INITIAL; + }; + + public var isFocusing: Bool { + self == .FOCUSING; + }; + + public var isBlurring: Bool { + self == .BLURRING; + }; + + public var isTransitioning: Bool { + self.isBlurring || self.isFocusing; + }; + + public var isBlurredOrBlurring: Bool { + self.isBlurred || self.isBlurring; + }; + + public var isFocusedOrFocusing: Bool { + self.isFocused || self.isFocusing; + }; +}; + +public struct RNIModalFocusStateMachine: RNIDictionaryRepresentable { + + // MARK: - Properties + // ------------------ + + private(set) public var state: RNIModalFocusState = .INITIAL; + private(set) public var statePrev: RNIModalFocusState = .INITIAL; + + public var wasBlurCancelled: Bool = false; + public var wasFocusCancelled: Bool = false; + + public var didChange: Bool { + self.statePrev != self.state; + }; + + // MARK: - RNIDictionaryRepresentable + // ---------------------------------- + + public var asDictionary: [String : Any] {[ + "state": self.state, + "statePrev": self.statePrev, + "isFocused": self.state.isFocused, + "isBlurred": self.state.isBlurred, + "isTransitioning": self.state.isTransitioning, + "wasBlurCancelled": self.wasBlurCancelled, + "wasFocusCancelled": self.wasFocusCancelled, + ]}; + + // MARK: - Functions + // ------------------ + + public mutating func set(state nextState: RNIModalFocusState){ + let prevState = self.state; + + // early exit if no change + guard prevState != nextState else { + #if DEBUG + print( + "Error - RNIModalFocusStateMachine.set" + + " - arg nextState: \(nextState)" + + " - prevState: \(prevState)" + + " - No changes, ignoring..." + ); + #endif + return; + }; + + if prevState.isInitialized && !nextState.isInitialized { + /// Going from "initialized state" (e.g. `FOCUSED`, `FOCUSING`, etc), to + /// an "uninitialized state" (i.e. `INITIAL`) is not allowed + return; + + } else { + self.state = nextState; + self.statePrev = prevState; + }; + + if prevState.isBlurring && nextState.isFocusing { + self.wasBlurCancelled = true; + + } else if prevState.isFocusing && nextState.isBlurring { + self.wasFocusCancelled = true; + }; + + #if DEBUG + print( + "Log - RNIModalFocusState.set" + + " - prevState: \(prevState)" + + " - nextState: \(nextState)" + + " - self.wasBlurCancelled: \(self.wasBlurCancelled)" + + " - self.wasFocusCancelled: \(self.wasFocusCancelled)" + ); + #endif + + self.resetIfNeeded(); + }; + + mutating func resetIfNeeded(){ + if self.state.isBlurred { + self.wasBlurCancelled = false; + }; + + if self.state.isFocused { + self.wasFocusCancelled = false; + }; + }; +}; diff --git a/ios/React Native/RNIModal/RNIModalManager.swift b/ios/React Native/RNIModal/RNIModalManager.swift new file mode 100644 index 00000000..9adfe8b7 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalManager.swift @@ -0,0 +1,448 @@ +// +// RNIModalManager.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/9/23. +// + +import UIKit + +public let RNIModalManagerShared = RNIModalManager.sharedInstance; + + +/// Archived/Old Notes +/// * `Note:2023-04-07-03-22-48` +/// * `Note:2023-03-30-19-36-33` +/// +public class RNIModalManager { + + // MARK: - Static Properties + // ------------------------- + + public static let sharedInstance = RNIModalManager(); + + // MARK: - Properties + // ------------------ + + private(set) public var modalInstanceDict = + RNIWeakDictionary(); + + private(set) public var windowToCurrentModalIndexMap: + Dictionary = [:]; + + // MARK: - Properties - Computed + // ----------------------------- + + public var modalInstances: [any RNIModal] { + self.modalInstanceDict.dict.compactMap { + $0.value.synthesizedRef; + }; + }; + + public var presentedModals: [any RNIModal] { + self.modalInstances.compactMap { + $0.modalPresentationState.isPresented ? $0 : nil; + }; + }; + + // MARK: - Methods + // --------------- + + public func register(modal: any RNIModal) { + modal.modalIndex = -1; + modal.modalIndexPrev = -1; + + modal.modalPresentationNotificationDelegate = self; + + self.modalInstanceDict[modal.modalNativeID] = modal; + }; + + public func isRegistered(modal: any RNIModal) -> Bool { + self.modalInstances.contains { + $0.modalNativeID == modal.modalNativeID; + }; + }; + + public func isRegistered(viewController: UIViewController) -> Bool { + self.modalInstances.contains { + $0.presentingViewController === viewController + }; + }; + + public func getModalInstance( + forPresentingViewController viewController: UIViewController + ) -> (any RNIModal)? { + self.modalInstances.first { + $0.presentingViewController === viewController; + }; + }; + + public func getModalInstance( + forPresentedViewController viewController: UIViewController + ) -> (any RNIModal)? { + let presentingModal = self.modalInstances.first { + $0.modalViewController === viewController; + }; + + guard let presentingVC = presentingModal?.modalViewController + else { return nil }; + + return self.getModalInstance(forPresentingViewController: presentingVC); + }; + + public func getModalInstances( + forWindow window: UIWindow + ) -> [any RNIModal] { + + return self.modalInstances.filter { + $0.window === window + }; + }; +}; + +// MARK: RNIModalPresentationNotifiable +// ------------------------------------ + +/// The modal instances will notify the manager when they're about to show/hide +/// a modal. +/// +extension RNIModalManager: RNIModalPresentationNotifiable { + + public func notifyOnModalWillShow(sender: any RNIModal) { + guard let senderWindow = sender.window else { + #if DEBUG + print( + "Error - RNIModalManager.notifyOnModalWillShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - Unable to send event because sender.window is nil" + ); + #endif + return; + }; + + let windowData = RNIModalWindowMapShared.get(forWindow: senderWindow); + guard windowData.nextModalToFocus !== sender else { + #if DEBUG + let nextModalToFocus = windowData.nextModalToFocus!; + print( + "Error - RNIModalManager.notifyOnModalWillShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToFocus.modalNativeID: \(nextModalToFocus.modalNativeID)" + + " - possible multiple invokation" + + " - sender is already about to be in focus" + ); + #endif + return; + }; + + let purgeCache = + RNIPresentedVCListCache.beginCaching(forWindow: senderWindow); + + /// `Note:2023-04-10-20-47-52` + /// * The sender will already be in `presentedModalList` despite it being + /// not fully presented yet. + /// + let presentedModalList = + RNIModalUtilities.getPresentedModals(forWindow: senderWindow); + + #if DEBUG + if windowData.nextModalToFocus != nil { + print( + "Warning - RNIModalManager.notifyOnModalWillShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToFocus is not nil" + + " - a different modal is about to be focused" + ); + }; + #endif + + windowData.set(nextModalToFocus: sender); + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalWillShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - prevModalIndex: \(windowData.modalIndexPrev)" + + " - windowData.modalIndexNext: \(windowData.modalIndexNext_)" + + " - sender.modalIndexPrev: \(sender.modalIndexPrev!)" + + " - sender.modalIndex: \(sender.modalIndex!)" + + " - presentedModalList.count: \(presentedModalList.count)" + ); + #endif + + sender.modalFocusState.set(state: .FOCUSING); + sender.modalPresentationState.set(state: .PRESENTING_UNKNOWN); + + sender.onModalWillFocusNotification(sender: sender); + + presentedModalList.forEach { + guard $0 !== sender, + $0.modalFocusState.state.isFocusedOrFocusing || + $0.modalFocusState.state.isBlurring + else { return }; + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalWillShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - notify BLURRING" + + " - for modal.modalNativeID:\($0.modalNativeID)" + + " - for modal.modalIndex:\($0.modalIndex ?? -1)" + ); + #endif + + $0.modalFocusState.set(state: .BLURRING); + $0.onModalWillBlurNotification(sender: sender); + }; + + if let modalToBlur = presentedModalList.secondToLast { + windowData.nextModalToBlur = modalToBlur; + }; + + purgeCache(); + }; + + public func notifyOnModalDidShow(sender: any RNIModal) { + guard let senderWindow = sender.window else { + #if DEBUG + print( + "Error - RNIModalManager.notifyOnModalDidShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - Unable to send event because sender.window is nil" + ); + #endif + return; + }; + + let windowData = RNIModalWindowMapShared.get(forWindow: senderWindow); + let nextModalToFocus = windowData.nextModalToFocus + + if nextModalToFocus == nil { + #if DEBUG + print( + "Error - RNIModalManager.notifyOnModalDidShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToFocus: nil" + + " - possible notifyOnModalWillShow not invoked for sender" + ); + #endif + }; + + #if DEBUG + if nextModalToFocus !== sender { + print( + "Warning - RNIModalManager.notifyOnModalDidShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToFocus.modalNativeID: \(nextModalToFocus?.modalNativeID ?? "N/A")" + + " - nextModalToFocus !== sender" + + " - a different modal is about to focused" + ); + }; + #endif + + let purgeCache = + RNIPresentedVCListCache.beginCaching(forWindow: senderWindow); + + let presentedModalList = + RNIModalUtilities.getPresentedModals(forWindow: senderWindow); + + windowData.apply(forFocusedModal: sender); + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalDidShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - sender.modalIndexPrev: \(sender.modalIndexPrev!)" + + " - sender.modalIndex: \(sender.modalIndex!)" + ); + #endif + + sender.modalFocusState.set(state: .FOCUSED); + sender.modalPresentationState.set(state: .PRESENTED_UNKNOWN); + + sender.onModalDidFocusNotification(sender: sender); + + presentedModalList.forEach { + guard $0 !== sender, + $0.modalFocusState.state.isBlurring + else { return }; + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalDidShow" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - notify BLURRED" + + " - for modal.modalNativeID:\($0.modalNativeID)" + + " - for modal.modalIndex:\($0.modalIndex ?? -1)" + ); + #endif + + $0.modalFocusState.set(state: .BLURRED); + $0.onModalDidBlurNotification(sender: sender); + }; + + // Reset + windowData.nextModalToBlur = nil; + purgeCache(); + }; + + public func notifyOnModalWillHide(sender: any RNIModal) { + guard let senderWindow = sender.window else { + #if DEBUG + print( + "Error - RNIModalManager.notifyOnModalWillHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - Unable to send event because sender.window is nil" + ); + #endif + return; + }; + + let windowData = RNIModalWindowMapShared.get(forWindow: senderWindow); + guard windowData.nextModalToBlur !== sender else { + #if DEBUG + let nextModalToBlur = windowData.nextModalToBlur!; + print( + "Error - RNIModalManager.notifyOnModalWillHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToBlur.modalNativeID: \(nextModalToBlur.modalNativeID)" + + " - possible multiple invokation" + + " - sender is already about to be blurred" + ); + #endif + return; + }; + + #if DEBUG + if windowData.nextModalToBlur != nil { + print( + "Warning - RNIModalManager.notifyOnModalWillHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToBlur is not nil" + + " - a different modal is about to blur" + ); + }; + #endif + + let purgeCache = + RNIPresentedVCListCache.beginCaching(forWindow: senderWindow); + + let presentedModalList = + RNIModalUtilities.getPresentedModals(forWindow: senderWindow); + + windowData.set(nextModalToBlur: sender); + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalWillHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - prevModalIndex: \(windowData.modalIndexPrev)" + + " - windowData.modalIndexNext: \(windowData.modalIndexNext_)" + + " - sender.modalIndexPrev: \(sender.modalIndexPrev!)" + + " - sender.modalIndex: \(sender.modalIndex!)" + ); + #endif + + sender.modalFocusState.set(state: .BLURRING); + sender.modalPresentationState.set(state: .DISMISSING_UNKNOWN); + + sender.onModalWillBlurNotification(sender: sender); + + guard let modalToFocus = presentedModalList.secondToLast else { + purgeCache(); + return; + }; + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalWillHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - notify FOCUSING" + + " - for modalToFocus.modalNativeID:\(modalToFocus.modalNativeID)" + + " - for modalToFocus.modalIndex:\(modalToFocus.modalIndex ?? -1)" + ); + #endif + + modalToFocus.modalFocusState.set(state: .FOCUSING); + modalToFocus.onModalWillFocusNotification(sender: sender); + + windowData.nextModalToFocus = modalToFocus; + purgeCache(); + }; + + public func notifyOnModalDidHide(sender: any RNIModal) { + guard let senderWindow = sender.window else { + #if DEBUG + print( + "Error - RNIModalManager.notifyOnModalDidHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - Unable to send event because sender.window is nil" + ); + #endif + return; + }; + + let windowData = RNIModalWindowMapShared.get(forWindow: senderWindow); + guard let nextModalToBlur = windowData.nextModalToBlur else { + #if DEBUG + print( + "Error - RNIModalManager.notifyOnModalDidHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToBlur: nil" + + " - possible notifyOnModalWillHide not invoked for sender" + ); + #endif + return; + }; + + #if DEBUG + if nextModalToBlur !== sender { + print( + "Warning - RNIModalManager.notifyOnModalDidHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - nextModalToBlur.modalNativeID: \(nextModalToBlur.modalNativeID)" + + " - nextModalToBlur !== sender" + + " - a different modal is about to be blurred" + ); + }; + #endif + + let purgeCache = + RNIPresentedVCListCache.beginCaching(forWindow: senderWindow); + + windowData.apply(forBlurredModal: sender); + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalDidHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - sender.modalIndexPrev: \(sender.modalIndexPrev!)" + + " - sender.modalIndex: \(sender.modalIndex!)" + ); + #endif + + sender.modalFocusState.set(state: .BLURRED); + sender.modalPresentationState.set(state: .DISMISSED); + + sender.onModalDidBlurNotification(sender: sender); + + guard let modalToFocus = windowData.nextModalToFocus else { return }; + + #if DEBUG + print( + "Log - RNIModalManager.notifyOnModalDidHide" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - notify FOCUSED" + + " - for modal.modalNativeID:\(modalToFocus.modalNativeID)" + + " - for modal.modalIndex:\(modalToFocus.modalIndex ?? -1)" + ); + #endif + + modalToFocus.modalFocusState.set(state: .FOCUSED); + modalToFocus.onModalDidFocusNotification(sender: sender); + + // reset + windowData.nextModalToFocus = nil; + purgeCache(); + }; +}; diff --git a/ios/React Native/RNIModal/RNIModalPresentationNotifiable.swift b/ios/React Native/RNIModal/RNIModalPresentationNotifiable.swift new file mode 100644 index 00000000..2287b353 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalPresentationNotifiable.swift @@ -0,0 +1,19 @@ +// +// RNIModalPresentationNotifiable.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/10/23. +// + +import UIKit + +public protocol RNIModalPresentationNotifiable: AnyObject { + + func notifyOnModalWillShow(sender: any RNIModal); + + func notifyOnModalDidShow(sender: any RNIModal); + + func notifyOnModalWillHide(sender: any RNIModal); + + func notifyOnModalDidHide(sender: any RNIModal); +}; diff --git a/ios/React Native/RNIModal/RNIModalPresentationState.swift b/ios/React Native/RNIModal/RNIModalPresentationState.swift new file mode 100644 index 00000000..8217fe6c --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalPresentationState.swift @@ -0,0 +1,303 @@ +// +// RNIModalPresentationState.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/7/23. +// + +import UIKit + + +public enum RNIModalPresentationState: String, CaseIterable { + + // MARK: - Enum Cases + // ------------------ + + case INITIAL; + + case PRESENTING_PROGRAMMATIC; + case PRESENTING_UNKNOWN; + + case PRESENTED; + case PRESENTED_UNKNOWN; + + case DISMISSING_GESTURE; + case DISMISSING_PROGRAMMATIC; + case DISMISSING_UNKNOWN; + + case DISMISS_GESTURE_CANCELLING; + + case DISMISSED; + + // MARK: - Computed Properties - Private + // ------------------------------------- + + fileprivate var step: Int { + switch self { + case .INITIAL: + return 0; + + case .PRESENTING_PROGRAMMATIC: fallthrough; + case .PRESENTING_UNKNOWN : + return 1; + + case .PRESENTED : fallthrough + case .PRESENTED_UNKNOWN: + return 2; + + case .DISMISSING_GESTURE : fallthrough; + case .DISMISSING_PROGRAMMATIC: fallthrough; + case .DISMISSING_UNKNOWN : + return 3; + + case .DISMISS_GESTURE_CANCELLING: + return 4; + + case .DISMISSED: + return 5; + }; + }; + + // MARK: - Computed Properties - Public + // ------------------------------------ + + public var isDismissing: Bool { + switch self { + case .DISMISSING_UNKNOWN, + .DISMISSING_GESTURE, + .DISMISSING_PROGRAMMATIC: + return true; + + default: + return false; + }; + }; + + public var isDismissingKnown: Bool { + self.isDismissing && self != .DISMISSING_UNKNOWN; + }; + + public var isPresenting: Bool { + switch self { + case .PRESENTING_PROGRAMMATIC, + .PRESENTING_UNKNOWN, + .DISMISS_GESTURE_CANCELLING: + return true; + + default: + return false; + }; + }; + + public var isPresented: Bool { + switch self { + case .PRESENTED, + .PRESENTED_UNKNOWN: + return true; + + default: + return false; + }; + }; + + public var isNotSpecific: Bool { + switch self { + case .PRESENTED_UNKNOWN, + .PRESENTING_UNKNOWN, + .DISMISSING_UNKNOWN: + return true; + + default: + return false; + }; + }; + + // MARK: - Computed Properties - Alias + // ----------------------------------- + + public var isDismissViaGestureCancelling: Bool { + self == .DISMISS_GESTURE_CANCELLING; + }; + + public var isDismissingViaGesture: Bool { + self == .DISMISSING_GESTURE + }; + + public var isDismissed: Bool { + self == .DISMISSED || self == .INITIAL; + }; + + // MARK: - Computed Properties - Composite + // --------------------------------------- + + public var isDismissedOrDismissing: Bool { + self.isDismissed || self.isDismissing; + }; + + public var isPresentedOrPresenting: Bool { + self.isPresented || self.isPresenting; + }; +}; + +public struct RNIModalPresentationStateMachine: RNIDictionaryRepresentable { + + // MARK: - Properties + // ------------------ + + private(set) public var state: RNIModalPresentationState = .INITIAL; + private(set) public var statePrev: RNIModalPresentationState = .INITIAL; + + // MARK: - Properties - Completion Handlers + // ---------------------------------------- + + public var onDismissWillCancel: (() -> Void)?; + public var onDismissDidCancel: (() -> Void)?; + + // MARK: - Properties + // ------------------ + + private var _isInitialPresent: Bool? = nil; + private var _wasCancelledDismiss: Bool = false; + + public var wasCancelledPresent: Bool = false; + public var wasCancelledDismissViaGesture: Bool = false; + + // MARK: - Computed Properties + // --------------------------- + + public var isInitialPresent: Bool { + self._isInitialPresent ?? true; + }; + + public var isPresented: Bool { + self.state.isPresented + }; + + public var wasCancelledDismiss: Bool { + self._wasCancelledDismiss || self.wasCancelledDismissViaGesture; + }; + + public var didChange: Bool { + self.statePrev != self.state; + }; + + // MARK: RNIDictionaryRepresentable + // -------------------------------- + + public var asDictionary: [String : Any] {[ + "state": self.state, + "statePrev": self.statePrev, + "isInitialPresent": self.isInitialPresent, + "isPresented": self.isPresented, + "isDismissed": self.state.isDismissed, + "wasCancelledDismiss": self.wasCancelledDismiss, + "wasCancelledPresent": self.wasCancelledPresent, + "wasCancelledDismissViaGesture": self.wasCancelledDismissViaGesture, + ]}; + + // MARK: - Functions + // ----------------- + + init( + onDismissWillCancel: ( () -> Void)? = nil, + onDismissDidCancel: ( () -> Void)? = nil + ) { + self.onDismissWillCancel = onDismissWillCancel; + self.onDismissDidCancel = onDismissDidCancel; + } + + public mutating func set(state nextState: RNIModalPresentationState){ + let prevState = self.state; + + guard prevState != nextState else { + // early exit if no change + return + }; + + let isBecomingUnknown = !prevState.isNotSpecific && nextState.isNotSpecific; + let isSameStep = prevState.step == nextState.step; + + /// Do not over-write specific/"known state", with non-specific/"unknown + /// state", e.g. + /// + /// * ✅: `PRESENTING_UNKNOWN` -> `PRESENTING_PROGRAMMATIC` + /// * ❌: `DISMISSING_GESTURE` -> `DISMISSING_UNKNOWN` + /// + if isSameStep { + if !isBecomingUnknown { + self.state = nextState; + }; + + #if DEBUG + print( + "Warning - RNIModalPresentationStateMachine.set" + + " - prevState: \(prevState)" + + " - nextState: \(nextState)" + ); + #endif + return; + }; + + self.statePrev = prevState; + + if prevState.isDismissingViaGesture && nextState.isPresenting { + self.wasCancelledDismissViaGesture = true; + self.state = .DISMISS_GESTURE_CANCELLING; + self.onDismissWillCancel?(); + + } else if prevState.isDismissViaGestureCancelling && nextState.isPresented { + self.state = nextState; + self.onDismissDidCancel?(); + + } else { + self.state = nextState; + }; + + self.updateProperties(); + self.resetIfNeeded(); + + #if DEBUG + print( + "Log - RNIModalPresentationStateMachine.set" + + " - statePrev: \(self.statePrev)" + + " - nextState: \(self.state)" + + " - isInitialPresent: \(self.isInitialPresent)" + + " - wasCancelledPresent: \(self.wasCancelledPresent)" + + " - wasCancelledDismiss: \(self.wasCancelledDismiss)" + + " - wasCancelledDismissViaGesture: \(self.wasCancelledDismissViaGesture)" + ); + #endif + }; + + private mutating func updateProperties(){ + let nextState = self.state; + let prevState = self.statePrev; + + if nextState.isPresenting && self._isInitialPresent == nil { + self._isInitialPresent = true; + + } else if nextState.isPresenting && self._isInitialPresent == true { + self._isInitialPresent = false; + }; + + if prevState.isPresenting && nextState.isDismissedOrDismissing { + self.wasCancelledPresent = true; + + } else if prevState.isDismissing && nextState.isPresentedOrPresenting { + self._wasCancelledDismiss = true; + }; + }; + + private mutating func resetIfNeeded(){ + if self.state.isPresented { + self.wasCancelledPresent = false; + + } else if self.state == .DISMISSED { + // reset + self.wasCancelledDismissViaGesture = false; + self.wasCancelledPresent = false; + self._isInitialPresent = nil; + self._wasCancelledDismiss = false; + }; + }; +}; diff --git a/ios/React Native/RNIModal/RNIModalPresentationTrigger.swift b/ios/React Native/RNIModal/RNIModalPresentationTrigger.swift new file mode 100644 index 00000000..c66c3b3d --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalPresentationTrigger.swift @@ -0,0 +1,16 @@ +// +// RNIModalPresentationTrigger.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/7/23. +// + +import UIKit + +public enum RNIModalPresentationTrigger: String { + case programmatic; + case gesture; + case request; + case reordering; + case unknown; +}; diff --git a/ios/React Native/RNIModal/RNIModalUtilities.swift b/ios/React Native/RNIModal/RNIModalUtilities.swift new file mode 100644 index 00000000..79335fb8 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalUtilities.swift @@ -0,0 +1,121 @@ +// +// RNIModalUtilities.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/1/23. +// + +import UIKit + +public class RNIModalUtilities { + + static func getPresentedModals( + forWindow window: UIWindow + ) -> [any RNIModal] { + + let vcItems = + RNIPresentedVCListCache.getPresentedViewControllers(forWindow: window); + + return vcItems.compactMap { + guard let modalVC = $0 as? RNIModalViewController else { return nil }; + return modalVC.modalViewRef; + }; + }; + + static func computeModalIndex( + forWindow window: UIWindow, + forViewController viewController: UIViewController? = nil + ) -> Int { + + let listPresentedVC = + RNIPresentedVCListCache.getPresentedViewControllers(forWindow: window); + + var index = -1; + + for vc in listPresentedVC { + if vc.presentingViewController != nil { + index += 1; + }; + + /// A - no `viewController` arg., keep counting until all items in + /// `listPresentedVC` have been exhausted. + guard viewController == nil else { continue }; + + /// B - `viewController` arg. specified, stop counting if found matching + /// instance of `viewController` in `listPresentedVC`. + guard viewController !== vc + else { break }; + }; + + return index; + }; + + static func computeModalIndex( + forWindow window: UIWindow?, + forViewController viewController: UIViewController? = nil + ) -> Int { + guard let window = window else { return -1 }; + + return Self.computeModalIndex( + forWindow: window, + forViewController: viewController + ); + }; + + static func getPresentedModal( + forPresentingViewController presentingVC: UIViewController, + presentedViewController presentedVC: UIViewController? = nil + ) -> (any RNIModal)? { + + let presentedVC = presentedVC ?? presentingVC.presentedViewController; + + /// A - `presentedVC` is a `RNIModalViewController`. + if let presentedModalVC = presentedVC as? RNIModalViewController { + return presentedModalVC.modalViewRef; + }; + + /// B - `presentingVC` is a `RNIModalViewController`. + if let presentingModalVC = presentingVC as? RNIModalViewController, + let presentingModal = presentingModalVC.modalViewRef, + let presentedModalVC = presentingModal.modalVC, + let presentedModal = presentedModalVC.modalViewRef { + + return presentedModal; + }; + + /// C - `presentedVC` has a corresponding `RNIModalViewControllerWrapper` + /// instance associated to it. + if let presentedVC = presentedVC, + let presentedModalWrapper = RNIModalViewControllerWrapperRegistry.get( + forViewController: presentedVC + ) { + + return presentedModalWrapper; + }; + + /// D - `presentingVC` has a `RNIModalViewControllerWrapper` instance + /// associated to it. + if let presentingModalWrapper = RNIModalViewControllerWrapperRegistry.get( + forViewController: presentingVC + ), + let presentedVC = presentingModalWrapper.modalViewController, + let presentedModalWrapper = RNIModalViewControllerWrapperRegistry.get( + forViewController: presentedVC + ) { + + return presentedModalWrapper; + }; + + let topmostVC = RNIUtilities.getTopmostPresentedViewController( + for: presentingVC.view.window + ); + + if let topmostModalVC = topmostVC as? RNIModalViewController, + let topmostModal = topmostModalVC.modalViewRef { + + return topmostModal; + }; + + return nil; + }; +}; diff --git a/ios/React Native/RNIModal/RNIModalWindowMap.swift b/ios/React Native/RNIModal/RNIModalWindowMap.swift new file mode 100644 index 00000000..437c4ed4 --- /dev/null +++ b/ios/React Native/RNIModal/RNIModalWindowMap.swift @@ -0,0 +1,140 @@ +// +// RNIModalWindowMap.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/8/23. +// + +import UIKit + + +internal class RNIWindowMapData { + + // MARK: - Properties + // ------------------ + + weak var window: UIWindow?; + + private(set) var modalIndexCurrent: Int = -1; + private(set) var modalIndexPrev: Int = -1; + private(set) var modalIndexNext: Int?; + + weak var nextModalToFocus: (any RNIModal)?; + weak var nextModalToBlur: (any RNIModal)?; + + #if DEBUG + var modalIndexNext_: String { + self.modalIndexNext == nil ? "N/A" : "\(self.modalIndexNext!)" + }; + #endif + + // MARK: - Functions + // ----------------- + + init(window: UIWindow){ + self.window = window; + }; + + func set(nextModalToFocus modalToFocus: any RNIModal) { + self.nextModalToFocus = modalToFocus; + + let modalIndexCurrent = + RNIModalUtilities.computeModalIndex(forWindow: modalToFocus.window); + + let modalIndexPrev = self.modalIndexCurrent; + + self.modalIndexCurrent = modalIndexCurrent; + self.modalIndexNext = modalIndexCurrent + 1; + self.modalIndexPrev = modalIndexPrev; + }; + + func apply(forFocusedModal focusedModal: any RNIModal) { + self.nextModalToFocus = nil; + + let modalIndexCurrent = + RNIModalUtilities.computeModalIndex(forWindow: focusedModal.window); + + let modalIndexPrev = self.modalIndexCurrent; + + self.modalIndexCurrent = modalIndexCurrent; + self.modalIndexNext = nil; + self.modalIndexPrev = modalIndexPrev; + + focusedModal.modalIndexPrev = modalIndexPrev; + focusedModal.modalIndex = modalIndexCurrent; + }; + + func set(nextModalToBlur modalToBlur: any RNIModal) { + self.nextModalToBlur = modalToBlur; + + let modalIndexCurrent = + RNIModalUtilities.computeModalIndex(forWindow: modalToBlur.window); + + let modalIndexPrev = self.modalIndexCurrent; + + self.modalIndexCurrent = modalIndexCurrent; + self.modalIndexNext = modalIndexCurrent - 1; + self.modalIndexPrev = modalIndexPrev; + }; + + func apply(forBlurredModal blurredModal: any RNIModal) { + self.nextModalToBlur = nil; + + let modalIndexCurrent = + RNIModalUtilities.computeModalIndex(forWindow: blurredModal.window); + + let modalIndexPrev = self.modalIndexCurrent; + + self.modalIndexCurrent = modalIndexCurrent; + self.modalIndexNext = nil; + self.modalIndexPrev = modalIndexPrev; + + blurredModal.modalIndexPrev = modalIndexPrev; + blurredModal.modalIndex = modalIndexCurrent; + }; + + // MARK: - Properties - Computed + // ----------------------------- + + var windowID: String? { + self.window?.synthesizedStringID; + }; +}; + +// MARK: - RNIModalWindowMap +// ------------------------- + +internal let RNIModalWindowMapShared = RNIModalWindowMap.sharedInstance; + +internal class RNIModalWindowMap { + + // MARK: - Properties - Static + // --------------------------- + + static var sharedInstance = RNIModalWindowMap(); + + // MARK: - Properties + // ------------------ + + private(set) var windowDataMap: Dictionary = [:]; + + // MARK: - Functions + // ----------------- + + func set(forWindow window: UIWindow, data: RNIWindowMapData){ + self.windowDataMap[window.synthesizedStringID] = data; + }; + + func get(forWindow window: UIWindow) -> RNIWindowMapData { + guard let windowData = self.windowDataMap[window.synthesizedStringID] else { + // No corresponding "modal index" for window yet, so initialize + // with value + let windowDataNew = RNIWindowMapData(window: window); + self.set(forWindow: window, data: windowDataNew); + + return windowDataNew; + }; + + return windowData; + }; +}; diff --git a/ios/React Native/RNIModal/RNIPresentedViewControllerCache.swift b/ios/React Native/RNIModal/RNIPresentedViewControllerCache.swift new file mode 100644 index 00000000..915190b2 --- /dev/null +++ b/ios/React Native/RNIModal/RNIPresentedViewControllerCache.swift @@ -0,0 +1,72 @@ +// +// RNIPresentedViewControllerCache.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/1/23. +// + +import UIKit + +let RNIPresentedVCListCache = RNIPresentedViewControllerCache.shared; + +class RNIPresentedViewControllerCache { + class Cache { + weak var targetWindow: UIWindow? = nil; + + var cacheRequestCount = 0; + + // note: this retains the vc instances... + var cache: [UIViewController]?; + + func clear(){ + self.targetWindow = nil; + self.cache = nil; + }; + }; + + static let shared = RNIPresentedViewControllerCache(); + + var map: Dictionary = [:]; + + func beginCaching(forWindow window: UIWindow?) -> () -> Void { + guard let window = window else { return {} }; + let windowID = window.synthesizedStringID; + + let cache: Cache = { + if let cache = self.map[windowID] { + return cache; + }; + + let newCache = Cache(); + newCache.targetWindow = window; + newCache.cache = RNIUtilities.getPresentedViewControllers(for: window); + + self.map[windowID] = newCache; + return newCache; + }(); + + cache.cacheRequestCount += 1; + + return { + cache.cacheRequestCount -= 1; + + guard cache.cacheRequestCount <= 0 else { return }; + + cache.clear(); + self.map.removeValue(forKey: windowID); + }; + }; + + func getPresentedViewControllers( + forWindow window: UIWindow? + ) -> [UIViewController] { + guard let windowID = window?.synthesizedStringID, + let cacheContainer = self.map[windowID], + let vcItemsCached = cacheContainer.cache + else { + return RNIUtilities.getPresentedViewControllers(for: window); + }; + + return vcItemsCached; + }; +}; diff --git a/ios/React Native/RNIModalView/RNIModalView.swift b/ios/React Native/RNIModalView/RNIModalView.swift new file mode 100644 index 00000000..4de3acc8 --- /dev/null +++ b/ios/React Native/RNIModalView/RNIModalView.swift @@ -0,0 +1,1285 @@ +// +// RNIModalView.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 6/9/20. +// Copyright © 2020 Facebook. All rights reserved. +// + +import Foundation +import React + +public class RNIModalView: + UIView, RNIIdentifiable, RNIModalIdentifiable, RNIModalPresentationNotifying, + RNIModalState, RNIModalPresentation { + + public typealias CompletionHandler = () -> Void; + + enum NativeIDKey: String { + case modalViewContent; + }; + + // MARK: - Properties - RNIIdentifiable + // ------------------------------------ + + public static var synthesizedIdPrefix: String = "modal-id-"; + + // MARK: - Properties + // ------------------ + + weak var bridge: RCTBridge?; + + var modalContentWrapper: RNIWrapperView?; + public var modalVC: RNIModalViewController?; + + public var sheetDetentStringCurrent: String?; + public var sheetDetentStringPrevious: String?; + + // MARK: - Properties - RNIModalPresentationNotifying + // -------------------------------------------------- + + public weak var modalPresentationNotificationDelegate: + RNIModalPresentationNotifiable!; + + // MARK: - Properties - RNIModalIdentifiable + // ----------------------------------------- + + public var modalUserID: String? { + self.modalID as? String + }; + + // MARK: - Properties - RNIModalState + // ---------------------------------- + + public var modalIndex: Int!; + public var modalIndexPrev: Int!; + + public lazy var modalPresentationState = RNIModalPresentationStateMachine( + onDismissWillCancel: { [unowned self] in + let eventData = self.synthesizedBaseEventData; + self.onModalDismissWillCancel?( + eventData.synthesizedJSDictionary + ); + }, + + onDismissDidCancel: { [unowned self] in + let eventData = self.synthesizedBaseEventData; + self.onModalDismissDidCancel?( + eventData.synthesizedJSDictionary + ); + } + ); + + public var modalFocusState = RNIModalFocusStateMachine(); + + // MARK: - Properties - RNIModalPresentation + // ----------------------------------------- + + public var modalViewController: UIViewController? { + self.modalVC; + }; + + public weak var presentingViewController: UIViewController?; + + // MARK: - Properties: React Props - Events + // ---------------------------------------- + + @objc var onModalWillPresent: RCTBubblingEventBlock?; + @objc var onModalDidPresent: RCTBubblingEventBlock?; + + @objc var onModalWillDismiss: RCTBubblingEventBlock?; + @objc var onModalDidDismiss: RCTBubblingEventBlock?; + + @objc var onModalWillShow: RCTBubblingEventBlock?; + @objc var onModalDidShow: RCTBubblingEventBlock?; + + @objc var onModalWillHide: RCTBubblingEventBlock?; + @objc var onModalDidHide: RCTBubblingEventBlock?; + + @objc var onModalWillFocus: RCTBubblingEventBlock?; + @objc var onModalDidFocus: RCTBubblingEventBlock?; + + @objc var onModalWillBlur: RCTBubblingEventBlock?; + @objc var onModalDidBlur: RCTBubblingEventBlock?; + + @objc var onPresentationControllerWillDismiss: RCTBubblingEventBlock?; + @objc var onPresentationControllerDidDismiss: RCTBubblingEventBlock?; + @objc var onPresentationControllerDidAttemptToDismiss: RCTBubblingEventBlock?; + + @objc var onModalDetentDidCompute: RCTBubblingEventBlock?; + @objc var onModalDidChangeSelectedDetentIdentifier: RCTBubblingEventBlock?; + + @objc var onModalDidSnap: RCTBubblingEventBlock?; + + @objc var onModalSwipeGestureStart: RCTBubblingEventBlock?; + @objc var onModalSwipeGestureDidEnd: RCTBubblingEventBlock?; + + @objc var onModalDismissWillCancel: RCTBubblingEventBlock?; + @objc var onModalDismissDidCancel: RCTBubblingEventBlock?; + + // MARK: - Properties: React Props - General + // ----------------------------------------- + + /// user-provided identifier for this modal + @objc var modalID: NSString? = nil; + + @objc var modalContentPreferredContentSize: NSDictionary? { + didSet { + self.modalVC?.setPreferredContentSize(withWindow: self.window); + } + }; + + // MARK: - Properties: React Props - BG-Related + // -------------------------------------------- + + @objc var isModalBGBlurred: Bool = true { + didSet { + guard oldValue != self.isModalBGBlurred else { return }; + self.modalVC?.isBGBlurred = self.isModalBGBlurred; + } + }; + + @objc var isModalBGTransparent: Bool = true { + didSet { + guard oldValue != self.isModalBGTransparent else { return }; + self.modalVC?.isBGTransparent = self.isModalBGTransparent; + } + }; + + @objc var modalBGBlurEffectStyle: NSString = "" { + didSet { + guard oldValue != self.modalBGBlurEffectStyle + else { return }; + + self.modalVC?.blurEffectStyle = self.synthesizedModalBGBlurEffectStyle; + } + }; + + // MARK: - Properties: React Props - Presentation/Transition + // --------------------------------------------------------- + + @objc var modalPresentationStyle: NSString = ""; + + @objc var modalTransitionStyle: NSString = ""; + + @objc var hideNonVisibleModals: Bool = false; + + /// control modal present/dismiss by mounting/un-mounting the react subview + /// * `true`: the modal is presented/dismissed when the view is mounted + /// or unmounted + /// + /// * `false`: the modal is presented/dismissed by calling the functions from + /// js/react + /// + @objc var presentViaMount: Bool = false; + + /// disable swipe gesture recognizer for this modal + @objc var enableSwipeGesture: Bool = true { + didSet { + let newValue = self.enableSwipeGesture; + guard newValue != oldValue else { return }; + + self.modalGestureRecognizer?.isEnabled = newValue; + } + }; + + /// allow modal to be programmatically closed even when not current focused + /// * `true`: the modal can be dismissed even when it's not the topmost + /// presented modal. + /// + /// * `false`: the modal can only be dismissed if it's in focus, otherwise + /// error. + /// + @objc var allowModalForceDismiss: Bool = true; + + @objc var isModalInPresentation: Bool = false { + willSet { + guard #available(iOS 13.0, *), + let vc = self.modalVC + else { return }; + + vc.isModalInPresentation = newValue + } + }; + + // MARK: - Properties: React Props - Sheet-Related + // ----------------------------------------------- + + @objc var modalSheetDetents: NSArray?; + + @objc var sheetPrefersScrollingExpandsWhenScrolledToEdge: Bool = true { + willSet { + guard #available(iOS 15.0, *), + let sheetController = self.sheetPresentationController + else { return }; + + sheetController.prefersScrollingExpandsWhenScrolledToEdge = newValue; + } + }; + + @objc var sheetPrefersEdgeAttachedInCompactHeight: Bool = false { + willSet { + guard #available(iOS 15.0, *), + let sheetController = self.sheetPresentationController + else { return }; + + self.sheetAnimateChangesIfNeeded { + sheetController.prefersEdgeAttachedInCompactHeight = newValue; + }; + } + }; + + @objc var sheetWidthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false { + willSet { + guard #available(iOS 15.0, *), + let sheetController = self.sheetPresentationController + else { return }; + + sheetController + .widthFollowsPreferredContentSizeWhenEdgeAttached = newValue; + } + }; + + @objc var sheetPrefersGrabberVisible: Bool = false { + willSet { + guard #available(iOS 15.0, *), + let sheetController = self.sheetPresentationController + else { return }; + + self.sheetAnimateChangesIfNeeded { + sheetController.prefersGrabberVisible = newValue; + }; + } + }; + + @objc var sheetShouldAnimateChanges: Bool = true; + + @objc var sheetLargestUndimmedDetentIdentifier: String? { + didSet { + guard #available(iOS 15.0, *), + let sheetController = self.sheetPresentationController + else { return }; + + self.sheetAnimateChangesIfNeeded { + sheetController.largestUndimmedDetentIdentifier = + self.synthesizedSheetLargestUndimmedDetentIdentifier; + }; + } + }; + + @objc var sheetPreferredCornerRadius: NSNumber? { + didSet { + let newValue = self.sheetPreferredCornerRadius; + + guard #available(iOS 15.0, *), + oldValue != newValue, + let sheetController = self.sheetPresentationController, + let cornerRadius = newValue?.doubleValue + else { return }; + + self.sheetAnimateChangesIfNeeded { + sheetController.preferredCornerRadius = cornerRadius; + }; + } + }; + + @objc var sheetSelectedDetentIdentifier: String? { + didSet { + let newValue = self.sheetSelectedDetentIdentifier; + + guard oldValue != newValue, + #available(iOS 15.0, *), + let sheetController = self.sheetPresentationController + else { return }; + + let nextDetentID = self.synthesizedSheetSelectedDetentIdentifier; + + self.sheetAnimateChangesIfNeeded { + sheetController.selectedDetentIdentifier = nextDetentID; + }; + + /// Delegate function does not get called when detent is changed via + /// setting `selectedDetentIdentifier`, so invoke manually... + /// + self.sheetPresentationControllerDidChangeSelectedDetentIdentifier( + sheetController + ); + } + }; + + // MARK: - Properties: Synthesized From Props + // ------------------------------------------ + + public var synthesizedModalContentPreferredContentSize: RNIComputableSize? { + guard let dict = self.modalContentPreferredContentSize, + let computableSize = RNIComputableSize(fromDict: dict) + else { return nil }; + + return computableSize; + }; + + public var synthesizedModalPresentationStyle: UIModalPresentationStyle { + let defaultStyle: UIModalPresentationStyle = { + guard #available(iOS 13.0, *) else { return .overFullScreen }; + return .automatic; + }(); + + guard let style = UIModalPresentationStyle( + string: self.modalPresentationStyle as String + ) else { + #if DEBUG + print( + "Error - RNIModalView.synthesizedModalPresentationStyle" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - Unable to parse presentation style string" + + " - modalPresentationStyle: '\(self.modalPresentationStyle)'" + + " - using default style: '\(defaultStyle)'" + ); + #endif + return defaultStyle; + }; + + switch style { + case .automatic, + .pageSheet, + .formSheet, + .fullScreen, + .overFullScreen: + + return style; + + default: + #if DEBUG + print( + "Error - RNIModalView.synthesizedModalPresentationStyle" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - Unsupported presentation style string value" + + " - modalPresentationStyle: '\(self.modalPresentationStyle)'" + + " - Using default style: '\(defaultStyle)'" + ); + #endif + return defaultStyle; + }; + }; + + public var synthesizedModalTransitionStyle: UIModalTransitionStyle { + let defaultStyle: UIModalTransitionStyle = .coverVertical ; + + // TODO:2023-03-22-13-18-14 - Refactor: Move `fromString` to enum init + guard let style = + UIModalTransitionStyle.fromString(self.modalTransitionStyle as String) + else { + #if DEBUG + print( + "Error - RNIModalView.synthesizedModalTransitionStyle " + + " - self.modalNativeID: \(self.modalNativeID)" + + " - Unable to parse string value" + + " - modalPresentationStyle: '\(self.modalPresentationStyle)'" + + " - Using default style: '\(defaultStyle)'" + ); + #endif + return defaultStyle; + }; + + return style; + }; + + public var synthesizedModalBGBlurEffectStyle: UIBlurEffect.Style { + // Provide default value + let defaultStyle: UIBlurEffect.Style = { + guard #available(iOS 13.0, *) else { return .light }; + return .systemThinMaterial; + }(); + + guard let blurStyle = UIBlurEffect.Style( + string: self.modalBGBlurEffectStyle as String + ) else { + #if DEBUG + print( + "Error - RNIModalView.synthesizedModalBGBlurEffectStyle" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - Unable to parse string value" + + " - modalPresentationStyle: '\(self.modalPresentationStyle)'" + + " - Using default style: '\(defaultStyle)'" + ); + #endif + return defaultStyle; + }; + + return blurStyle; + }; + + @available(iOS 15.0, *) + public var synthesizedModalSheetDetents: [UISheetPresentationController.Detent]? { + self.modalSheetDetents?.compactMap { + if let string = $0 as? String, + let detent = UISheetPresentationController.Detent.fromString(string) { + + return detent; + + } else if #available(iOS 16.0, *), + let dict = $0 as? Dictionary { + + let customDetent = RNIModalCustomSheetDetent(forDict: dict) { + _, maximumDetentValue, computedDetentValue, sender in + + let eventData = RNIModalDetentDidComputeEventData( + maximumDetentValue: maximumDetentValue, + computedDetentValue: computedDetentValue, + key: sender.key + ); + + self.onModalDetentDidCompute?( + eventData.synthesizedJSDictionary + ); + }; + + return customDetent?.synthesizedDetent; + }; + + return nil; + }; + }; + + @available(iOS 15.0, *) + public var synthesizedSheetLargestUndimmedDetentIdentifier: + UISheetPresentationController.Detent.Identifier? { + + guard let identifierString = self.sheetLargestUndimmedDetentIdentifier + else { return nil }; + + return UISheetPresentationController.Detent.Identifier( + fromString: identifierString + ); + }; + + @available(iOS 15.0, *) + public var synthesizedSheetSelectedDetentIdentifier: + UISheetPresentationController.Detent.Identifier? { + + guard let identifierString = self.sheetSelectedDetentIdentifier + else { return nil }; + + return UISheetPresentationController.Detent.Identifier( + fromString: identifierString + ); + }; + + // MARK: - Properties: Synthesized + // ------------------------------- + + public var synthesizedBaseEventData: RNIModalBaseEventData { + RNIModalBaseEventData( + reactTag: self.reactTag.intValue, + modalID: self.modalID as? String, + modalData: self.synthesizedModalData + ); + }; + + // MARK: - Properties: Computed + // ---------------------------- + + // TODO: Move to `RNIModal+Helpers` + @available(iOS 15.0, *) + var sheetPresentationController: UISheetPresentationController? { + guard let presentedVC = self.modalViewController else { return nil }; + + switch presentedVC.modalPresentationStyle { + case .popover: + return presentedVC.popoverPresentationController? + .adaptiveSheetPresentationController; + + case .automatic, + .formSheet, + .pageSheet: + return presentedVC.sheetPresentationController; + + default: + return nil; + }; + }; + + @available(iOS 15.0, *) + var currentSheetDetentID: UISheetPresentationController.Detent.Identifier? { + guard let sheetController = self.sheetPresentationController + else { return nil }; + + let detents = sheetController.detents; + + if let selectedDetent = sheetController.selectedDetentIdentifier { + return selectedDetent; + + } else if #available(iOS 16.0, *), + let firstDetent = detents.first { + + /// The default value of `selectedDetentIdentifier` is nil, which means + /// the sheet displays at the smallest detent you specify in detents. + return firstDetent.identifier; + }; + + return nil; + }; + + var currentSheetDetentString: String? { + guard #available(iOS 15.0, *) else { return nil }; + return currentSheetDetentID?.rawValue; + }; + + var isModalViewPresentationNotificationEnabled: Bool { + RNIModalFlagsShared.isModalViewPresentationNotificationEnabled + }; + + var modalGestureRecognizer: UIGestureRecognizer? { + guard let modalVC = self.modalVC, + let controller = modalVC.presentationController, + let gestureRecognizers = controller.presentedView?.gestureRecognizers + else { return nil }; + + return gestureRecognizers.first; + }; + + var debugData: Dictionary { + self.synthesizedModalData.synthesizedJSDictionary + }; + + // MARK: - Init + // ------------ + + init(bridge: RCTBridge) { + super.init(frame: CGRect()); + + self.bridge = bridge; + RNIModalManagerShared.register(modal: self); + }; + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder); + fatalError("Not implemented"); + }; + + // MARK: - UIKit Lifecycle + // ----------------------- + + public override func didMoveToWindow() { + super.didMoveToWindow(); + + if self.presentViaMount { + try? self.dismissModal(); + }; + }; + + public override func didMoveToSuperview() { + super.didMoveToSuperview(); + + if self.presentViaMount { + try? self.presentModal(); + }; + }; + + // MARK: - React-Native Lifecycle + // ------------------------------ + + public override func insertReactSubview(_ subview: UIView!, at atIndex: Int) { + super.insertReactSubview(subview, at: atIndex); + + guard let wrapperView = subview as? RNIWrapperView, + let nativeID = subview.nativeID, + let nativeIDKey = NativeIDKey(rawValue: nativeID) + else { return }; + + self.initControllers(); + wrapperView.isMovingToParent = true; + + switch nativeIDKey { + case .modalViewContent: + guard self.modalContentWrapper !== wrapperView, + self.modalContentWrapper?.reactTag != wrapperView.reactTag + else { return }; + + if let oldModalContentWrapper = self.modalContentWrapper { + + oldModalContentWrapper.cleanup(); + self.modalContentWrapper = nil; + self.deinitControllers(); + }; + + self.modalContentWrapper = wrapperView; + self.initControllers(); + }; + + wrapperView.removeFromSuperview(); + wrapperView.isMovingToParent = false; + }; + + public override func removeReactSubview(_ subview: UIView!) { + super.removeReactSubview(subview); + }; + + // MARK: - Functions - Private + // ---------------------------- + + private func initControllers(){ + #if DEBUG + print( + "Log - RNIModalView.initControllers" + + " - self.modalNativeID: '\(self.modalNativeID)'" + ); + #endif + + self.modalVC = { + let vc = RNIModalViewController(); + + vc.modalViewRef = self; + vc.lifecycleDelegate = self; + + vc.isBGBlurred = self.isModalBGBlurred; + vc.isBGTransparent = self.isModalBGTransparent; + + vc.blurEffectStyle = self.synthesizedModalBGBlurEffectStyle; + + return vc; + }(); + }; + + private func deinitControllers(){ + #if DEBUG + print( + "Log - RNIModalView.deinitControllers" + + " - self.modalNativeID: '\(self.modalNativeID)'" + ); + #endif + + self.modalVC = nil; + self.presentingViewController = nil; + }; + + private func setupOnModalInitialPresent(){ + guard let panGesture = self.modalGestureRecognizer else { return }; + + panGesture.addTarget(self, + action: #selector(Self.handleGestureRecognizer(_:)) + ); + }; + + private func notifyIfModalDismissCancelled(){ + guard let modalVC = self.modalVC, + let transitionCoordinator = modalVC.transitionCoordinator + else { return }; + + transitionCoordinator.notifyWhenInteractionChanges { + guard $0.isCancelled else { return }; + + self.modalPresentationState.set(state: .DISMISS_GESTURE_CANCELLING); + self.modalPresentationState.wasCancelledDismissViaGesture = true; + + self.modalPresentationNotificationDelegate + .notifyOnModalWillShow(sender: self); + }; + + self.modalVC?.transitionCoordinator?.animate(alongsideTransition: nil) { + guard $0.isCancelled else { return }; + + self.modalPresentationNotificationDelegate + .notifyOnModalDidShow(sender: self); + }; + }; + + @objc private func handleGestureRecognizer(_ sender: UIGestureRecognizer) { + guard let window = self.window else { return }; + + switch sender.state { + case .began: + let gestureEventData = RNIModalSwipeGestureEventData( + position: sender.location(in: window) + ); + + self.onModalSwipeGestureStart?( + gestureEventData.synthesizedJSDictionary + ); + + case .ended: + let gestureEventData = RNIModalSwipeGestureEventData( + position: sender.location(in: window) + ); + + self.onModalSwipeGestureDidEnd?( + gestureEventData.synthesizedJSDictionary + ); + + guard let presentationController = self.modalVC?.presentationController, + let presentedView = presentationController.presentedView, + let positionAnimation = + presentedView.layer.animation(forKey: "position") + else { break }; + + positionAnimation.waitUntiEnd { + let eventData = RNIModalDidSnapEventData( + selectedDetentIdentifier: self.currentSheetDetentString, + modalContentSize: presentedView.bounds.size + ); + + self.onModalDidSnap?( + eventData.synthesizedJSDictionary + ); + }; + + default: break; + }; + }; + + /// `TODO:2023-03-22-12-07-54` + /// * Refactor: Move to `RNIModalManager` + /// + /// helper func to hide/show the other modals that are below level + private func setIsHiddenForViewBelowLevel(_ level: Int, isHidden: Bool){ + let presentedVCList = + RNIPresentedVCListCache.getPresentedViewControllers(forWindow: window); + + for (index, vc) in presentedVCList.enumerated() { + if index < level { + vc.view.isHidden = isHidden; + }; + }; + }; + + @available(iOS 15.0, *) + private func sheetAnimateChangesIfNeeded(_ block: () -> Void){ + guard let sheetController = self.sheetPresentationController + else { return }; + + if self.sheetShouldAnimateChanges { + sheetController.animateChanges { + block(); + }; + + } else { + block(); + }; + }; + + @available(iOS 15.0, *) + private func applyModalSheetProps( + to sheetController: UISheetPresentationController + ){ + + if let detents = self.synthesizedModalSheetDetents, detents.count >= 1 { + sheetController.detents = detents; + }; + + sheetController.prefersScrollingExpandsWhenScrolledToEdge = + self.sheetPrefersScrollingExpandsWhenScrolledToEdge; + + sheetController.prefersEdgeAttachedInCompactHeight = + self.sheetPrefersEdgeAttachedInCompactHeight; + + sheetController.widthFollowsPreferredContentSizeWhenEdgeAttached = + self.sheetWidthFollowsPreferredContentSizeWhenEdgeAttached; + + sheetController.prefersGrabberVisible = self.sheetPrefersGrabberVisible; + + sheetController.largestUndimmedDetentIdentifier = + self.synthesizedSheetLargestUndimmedDetentIdentifier; + + if let cornerRadius = self.sheetPreferredCornerRadius?.doubleValue { + sheetController.preferredCornerRadius = cornerRadius; + }; + + sheetController.selectedDetentIdentifier = + self.synthesizedSheetSelectedDetentIdentifier; + }; + + // MARK: - Functions - Public + // -------------------------- + + public func presentModal( + animated: Bool = true, + completion: CompletionHandler? = nil + ) throws { + guard self.window != nil else { + throw RNIModalError( + code: .runtimeError, + message: "Guard check failed, window is nil", + debugData: self.debugData + ); + }; + + guard !self.computedIsModalPresented else { + throw RNIModalError( + code: .modalAlreadyVisible, + message: "Guard check failed, modal already presented", + debugData: self.debugData + ); + }; + + let presentedViewControllers = + RNIPresentedVCListCache.getPresentedViewControllers(forWindow: window); + + guard let topMostPresentedVC = presentedViewControllers.last else { + throw RNIModalError( + code: .runtimeError, + message: "Guard check failed, could not get topMostPresentedVC", + debugData: self.debugData + ); + }; + + guard let modalVC = self.modalVC else { + throw RNIModalError( + code: .runtimeError, + message: "Guard check failed, could not get modalVC", + debugData: self.debugData + ); + }; + + modalVC.modalTransitionStyle = self.synthesizedModalTransitionStyle; + modalVC.modalPresentationStyle = self.synthesizedModalPresentationStyle; + + if #available(iOS 15.0, *), + let sheetController = self.sheetPresentationController { + + sheetController.delegate = self; + self.applyModalSheetProps(to: sheetController); + }; + + #if DEBUG + print( + "Log - RNIModalView.presentModal - Start presenting" + + " - self.reactTag: \(self.reactTag ?? -1)" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - self.modalIndex: \(self.modalIndex!)" + + " - self.modalID: \(self.modalID ?? "N/A")" + + " - self.presentationStyle: \(self.synthesizedModalPresentationStyle)" + + " - self.transitionStyle: \(self.synthesizedModalTransitionStyle)" + ); + #endif + + // Temporarily disable swipe gesture while it's being presented + self.modalGestureRecognizer?.isEnabled = false; + + self.presentingViewController = topMostPresentedVC; + + /// set specific "presenting" state + self.modalPresentationState.set(state: .PRESENTING_PROGRAMMATIC); + + self.onModalWillPresent?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + + topMostPresentedVC.present(modalVC, animated: animated) { [unowned self] in + + // Become the modal's presentation delegate after it has been presented + // in order to not override system-defined default modal behavior + modalVC.presentationController?.delegate = self; + + // Reset swipe gesture before it was temporarily disabled + self.modalGestureRecognizer?.isEnabled = self.enableSwipeGesture; + + self.modalPresentationState.set(state: .PRESENTED); + + self.onModalDidPresent?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + + completion?(); + + #if DEBUG + print( + "Log - RNIModalView.presentModal - Present modal finished" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - self.modalIndex: \(self.modalIndex!)" + + " - self.modalID: \(self.modalID ?? "N/A")" + ); + #endif + }; + }; + + public func dismissModal( + animated: Bool = true, + completion: CompletionHandler? = nil + ) throws { + guard self.computedIsModalPresented else { + throw RNIModalError( + code: .modalAlreadyHidden, + message: "Guard check failed, modal already dismissed", + debugData: self.debugData + ); + }; + + guard let modalVC = self.modalVC else { + throw RNIModalError( + code: .runtimeError, + message: "Guard check failed, Unable to get modalVC", + debugData: self.debugData + ); + }; + + let isModalInFocus = self.computedIsModalInFocus; + + let shouldDismiss = isModalInFocus + ? true + : self.allowModalForceDismiss; + + guard shouldDismiss else { + throw RNIModalError( + code: .dismissRejected, + message: "Guard check failed, shouldDismiss prop is false", + debugData: self.debugData + ); + }; + + /// TODO:2023-03-22-12-12-05 - Remove? + let presentedVC: UIViewController = isModalInFocus + ? modalVC + : modalVC.presentingViewController! + + + #if DEBUG + print( + "Log - RNIModalView.dismissModal" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - Start dismissing modal" + ); + #endif + + self.modalGestureRecognizer?.isEnabled = false; + + /// set specific "dismissing" state + self.modalPresentationState.set(state: .DISMISSING_PROGRAMMATIC); + + self.onModalWillDismiss?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + + presentedVC.dismiss(animated: animated){ + self.modalPresentationState.set(state: .DISMISSED); + + self.onModalDidDismiss?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + + completion?(); + + #if DEBUG + print( + "Log - RNIModalView.dismissModal" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - Dismiss modal finished" + ); + #endif + }; + }; +}; + +// MARK: - UIAdaptivePresentationControllerDelegate +// ------------------------------------------------ + +/// `Note:2023-03-31-16-48-10` +/// +/// * The "blur/focus"-related events in +/// `UIAdaptivePresentationControllerDelegate` only fire in response to user +/// gestures (i.e. if the user swiped the modal away). +/// +/// * In other words, if the modal was closed programmatically, the +/// `UIAdaptivePresentationControllerDelegate`-related events will not get +/// invoked. +/// +extension RNIModalView: UIAdaptivePresentationControllerDelegate { + + /// `Note:2023-03-31-17-01-57` + /// + /// * This gets called whenever the user starts swiping the modal down, + /// regardless of whether or not the swipe action was cancelled half-way + /// through. + /// + /// * Only called when the sheet is dismissed by DRAGGING. + /// + /// + /// `Note:2023-04-01-14-52-05` + /// + /// * Invocation history when a modal is dismissed via a swipe gesture, but + /// was cancelled half-way + /// + /// * A - Swipe dismiss gesture begin... + /// * 1 - `presentationControllerWillDismiss + /// * 2 - `viewWillDisappear` + /// * B - Swipe dismiss gesture cancelled... + /// * 3 - `viewWillAppear` + /// * 4 - `viewDidAppear` + /// + public func presentationControllerWillDismiss( + _ presentationController: UIPresentationController + ) { + self.modalPresentationState.set(state: .DISMISSING_GESTURE); + + #if DEBUG + print( + "Log - RNIModalView+UIAdaptivePresentationControllerDelegate" + + " - RNIModalView.presentationControllerWillDismiss" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - self.modalIndex: \(self.modalIndex!)" + ); + #endif + }; + + public func presentationControllerDidDismiss( + _ presentationController: UIPresentationController + ) { + self.modalPresentationNotificationDelegate + .notifyOnModalDidHide(sender: self); + + #if DEBUG + print( + "Log - RNIModalView+UIAdaptivePresentationControllerDelegate" + + " - RNIModalView.presentationControllerDidDismiss" + + " - self.modalNativeID: \(self.modalNativeID)" + ); + #endif + }; + + /// `Note:2023-04-07-01-28-57` + /// No other "view controller"-related lifecycle method was trigger in + /// response to this event being invoked. + /// + public func presentationControllerDidAttemptToDismiss( + _ presentationController: UIPresentationController + ) { + + #if DEBUG + print( + "Log - RNIModalView+UIAdaptivePresentationControllerDelegate" + + " - RNIModalView.presentationControllerDidAttemptToDismiss" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - self.modalIndex: \(self.modalIndex!)" + ); + #endif + }; +}; + +// MARK: - UISheetPresentationControllerDelegate +// --------------------------------------------- + +@available(iOS 15.0, *) +extension RNIModalView: UISheetPresentationControllerDelegate { + + /// `Note:2023-04-22-03-50-59` + /// + /// * This function gets invoked when the sheet has snapped into a detent + /// + /// * However, we don't get notified whenever the user is currently dragging + /// the sheet. + /// + /// * The `presentedViewController.transitionCoordinator` is only available + /// during modal presentation and dismissal. + /// + /// + public func sheetPresentationControllerDidChangeSelectedDetentIdentifier( + _ sheetPresentationController: UISheetPresentationController + ) { + let currentDetentID = self.currentSheetDetentID; + + self.sheetDetentStringPrevious = self.sheetDetentStringCurrent; + self.sheetDetentStringCurrent = currentDetentID?.description; + + #if DEBUG + print( + "Log - RNIModalView+UISheetPresentationControllerDelegate" + + " - sheetPresentationControllerDidChangeSelectedDetentIdentifier" + + " - sheetDetentStringPrevious: \(self.sheetDetentStringPrevious ?? "N/A")" + + " - sheetDetentStringCurrent: \(self.sheetDetentStringCurrent ?? "N/A")" + ); + #endif + + let eventData = RNIModalDidChangeSelectedDetentIdentifierEventData( + sheetDetentStringPrevious: self.sheetDetentStringPrevious, + sheetDetentStringCurrent: self.sheetDetentStringCurrent + ); + + self.onModalDidChangeSelectedDetentIdentifier?( + eventData.synthesizedJSDictionary + ); + }; +}; + +// MARK: Extension: RNIModalRequestable +// ------------------------------------ + +extension RNIModalView: RNIModalRequestable { + + public func requestModalToShow( + sender: Any, + animated: Bool, + completion: @escaping () -> Void + ) throws { + try self.presentModal(animated: animated, completion: completion); + }; + + public func requestModalToHide( + sender: Any, + animated: Bool, + completion: @escaping () -> Void + ) throws { + try self.dismissModal(animated: animated, completion: completion); + }; +}; + +// MARK: - RNIViewControllerLifeCycleNotifiable +// -------------------------------------------- + +extension RNIModalView: RNIViewControllerLifeCycleNotifiable { + + public func viewWillAppear(sender: UIViewController, animated: Bool) { + guard sender.isBeingPresented else { return }; + self.modalPresentationState.set(state: .PRESENTING_UNKNOWN); + + if self.modalPresentationState.didChange { + self.onModalWillShow?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + }; + + if self.isModalViewPresentationNotificationEnabled { + self.modalPresentationNotificationDelegate + .notifyOnModalWillShow(sender: self); + }; + + if self.modalPresentationState.isInitialPresent { + self.setupOnModalInitialPresent(); + }; + }; + + public func viewDidAppear(sender: UIViewController, animated: Bool) { + guard sender.isBeingPresented else { return }; + self.modalPresentationState.set(state: .PRESENTED_UNKNOWN); + + if self.modalPresentationState.didChange { + self.onModalDidShow?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + }; + + if self.isModalViewPresentationNotificationEnabled { + self.modalPresentationNotificationDelegate + .notifyOnModalDidShow(sender: self); + }; + }; + + public func viewWillDisappear(sender: UIViewController, animated: Bool) { + guard sender.isBeingDismissed else { return }; + self.modalPresentationState.set(state: .DISMISSING_UNKNOWN); + + if self.modalPresentationState.didChange { + self.onModalWillHide?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + }; + + if self.isModalViewPresentationNotificationEnabled { + self.modalPresentationNotificationDelegate + .notifyOnModalWillHide(sender: self); + }; + + self.notifyIfModalDismissCancelled(); + }; + + public func viewDidDisappear(sender: UIViewController, animated: Bool) { + guard sender.isBeingDismissed else { return }; + self.modalPresentationState.set(state: .DISMISSED); + + if self.modalPresentationState.didChange { + self.onModalDidHide?( + self.synthesizedBaseEventData.synthesizedJSDictionary + ); + }; + + if self.isModalViewPresentationNotificationEnabled { + self.modalPresentationNotificationDelegate + .notifyOnModalDidHide(sender: self); + }; + + self.deinitControllers(); + }; +}; + + +// MARK: Extension: RNIModalFocusNotifiable +// ---------------------------------------- + +/// `Note:2023-03-31-17-51-56` +extension RNIModalView: RNIModalFocusNotifiable { + + public func onModalWillFocusNotification(sender: any RNIModal) { + guard self.modalFocusState.didChange else { return }; + + let eventData = RNIOnModalFocusEventData( + modalData: self.synthesizedBaseEventData, + senderInfo: sender.synthesizedModalData, + isInitial: sender === self + ); + + self.onModalWillFocus?( + eventData.synthesizedJSDictionary + ); + }; + + public func onModalDidFocusNotification(sender: any RNIModal) { + guard self.modalFocusState.didChange else { return }; + + let eventData = RNIOnModalFocusEventData( + modalData: self.synthesizedBaseEventData, + senderInfo: sender.synthesizedModalData, + isInitial: sender === self + ); + + self.onModalDidFocus?( + eventData.synthesizedJSDictionary + ); + + #if DEBUG + print( + "Log - RNIModalView.onModalDidFocusNotification" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - self.modalIndex: \(self.modalIndex!)" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - arg sender.modalIndex: \(sender.modalIndex!)" + ); + #endif + + }; + + public func onModalWillBlurNotification(sender: any RNIModal) { + guard self.modalFocusState.didChange else { return }; + + let eventData = RNIOnModalFocusEventData( + modalData: self.synthesizedBaseEventData, + senderInfo: sender.synthesizedModalData, + isInitial: sender === self + ); + + self.onModalWillBlur?( + eventData.synthesizedJSDictionary + ); + }; + + public func onModalDidBlurNotification(sender: any RNIModal) { + guard self.modalFocusState.didChange else { return }; + + let eventData = RNIOnModalFocusEventData( + modalData: self.synthesizedBaseEventData, + senderInfo: sender.synthesizedModalData, + isInitial: sender === self + ); + + self.onModalDidBlur?( + eventData.synthesizedJSDictionary + ); + + #if DEBUG + print( + "Log - RNIModalView.onModalDidBlurNotification" + + " - self.modalNativeID: \(self.modalNativeID)" + + " - self.modalIndex: \(self.modalIndex!)" + + " - arg sender.modalNativeID: \(sender.modalNativeID)" + + " - arg sender.modalIndex: \(sender.modalIndex!)" + ); + #endif + }; +}; diff --git a/ios/React Native/RNIModalView/RNIModalViewController.swift b/ios/React Native/RNIModalView/RNIModalViewController.swift new file mode 100644 index 00000000..941f1640 --- /dev/null +++ b/ios/React Native/RNIModalView/RNIModalViewController.swift @@ -0,0 +1,369 @@ +// +// RNIModalViewController.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 6/9/20. +// Copyright © 2020 Facebook. All rights reserved. +// + +import Foundation + +public class RNIModalViewController: UIViewController { + + // MARK: - Properties + // ------------------ + + weak var modalViewRef: RNIModalView?; + weak var lifecycleDelegate: RNIViewControllerLifeCycleNotifiable?; + + var isBGTransparent: Bool = true { + didSet { + guard oldValue != self.isBGTransparent else { return }; + self.updateBackgroundTransparency(); + self.updateBackgroundBlur(); + } + }; + var isBGBlurred: Bool = true { + didSet { + guard oldValue != self.isBGBlurred else { return }; + self.updateBackgroundBlur(); + } + }; + + private(set) public var prevBounds: CGRect?; + + private var initialSize: CGSize?; + + // MARK: - Properties - Computed + // ----------------------------- + + var modalID: String? { + self.modalViewRef?.modalID as? String + }; + + var modalContentWrapper: RNIWrapperView? { + self.modalViewRef?.modalContentWrapper; + }; + + var computablePreferredContentSize: RNIComputableSize? { + self.modalViewRef?.synthesizedModalContentPreferredContentSize; + }; + + var shouldUpdateContentSize: Bool { + guard let computableSize = self.computablePreferredContentSize, + case .current = computableSize.mode + else { return true }; + + return false; + }; + + // MARK: - Properties + // ------------------ + + var blurEffectView: UIVisualEffectView? = nil; + + var blurEffectStyle: UIBlurEffect.Style? { + didSet { + let didChange = oldValue != blurEffectStyle; + let isPresented = self.presentingViewController != nil; + + guard didChange && isPresented, + let blurEffectStyle = self.blurEffectStyle else { return }; + + #if DEBUG + print( + "Log - RNIModalViewController.blurEffectStyle - didSet" + + " - modalNativeID: '\(self.modalViewRef?.modalNativeID ?? "N/A")'" + + " - oldValue: '\(oldValue!.description)'" + + " - newValue: '\(blurEffectStyle.description)'" + ); + #endif + + self.updateBackgroundBlur(); + } + }; + + // MARK: - View Controller Lifecycle + // --------------------------------- + + public override func viewDidLoad() { + super.viewDidLoad(); + + self.view = { + let view = UIView(); + view.autoresizingMask = [.flexibleHeight, .flexibleWidth]; + + return view; + }(); + + if let modalContentWrapper = self.modalContentWrapper { + self.view.addSubview(modalContentWrapper); + modalContentWrapper.notifyForBoundsChange(size: self.view.bounds.size); + }; + + self.updateBackgroundTransparency(); + self.updateBackgroundBlur(); + + self.lifecycleDelegate?.viewDidLoad(sender: self); + }; + + public override func viewDidLayoutSubviews(){ + super.viewDidLayoutSubviews(); + + let didChangeBounds: Bool = { + guard let prevBounds = self.prevBounds else { return true }; + return !prevBounds.equalTo(self.view.bounds); + }(); + + guard didChangeBounds, + let modalContentWrapper = self.modalContentWrapper + else { return }; + + let nextBounds = self.view.bounds; + + let prevBounds = self.prevBounds; + self.prevBounds = nextBounds; + + #if DEBUG + print( + "Log - RNIModalViewController.viewDidLayoutSubviews" + + " - modalNativeID: '\(self.modalViewRef?.modalNativeID ?? "N/A")'" + + " - self.prevBounds: \(String(describing: prevBounds))" + + " - nextBounds: \(nextBounds)" + ); + #endif + + if self.shouldUpdateContentSize { + modalContentWrapper.notifyForBoundsChange(size: nextBounds.size); + modalContentWrapper.center = self.view.center; + }; + + self.lifecycleDelegate?.viewDidLayoutSubviews(sender: self); + }; + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated); + + self.lifecycleDelegate? + .viewWillAppear(sender: self, animated: animated); + + self.setPreferredContentSize(); + + #if DEBUG + print( + "Log - RNIModalViewController.viewWillAppear" + + " - arg animated: \(animated)" + + " - self.modalNativeID: \(self.modalViewRef?.modalNativeID ?? "N/A")" + + " - self.isBeingPresented: \(self.isBeingPresented)" + ); + #endif + }; + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated); + + self.lifecycleDelegate? + .viewDidAppear(sender: self, animated: animated); + + #if DEBUG + print( + "Log - RNIModalViewController.viewDidAppear" + + " - arg animated: \(animated)" + + " - self.modalNativeID: \(self.modalViewRef?.modalNativeID ?? "N/A")" + + " - self.isBeingPresented: \(self.isBeingPresented)" + ); + #endif + }; + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated); + + self.lifecycleDelegate? + .viewWillDisappear(sender: self, animated: animated); + + #if DEBUG + print( + "Log - RNIModalViewController.viewWillDisappear" + + " - arg animated: \(animated)" + + " - self.modalNativeID: \(self.modalViewRef?.modalNativeID ?? "N/A")" + + " - self.isBeingDismissed: \(self.isBeingDismissed)" + + " - self.transitionCoordinator: \(String(describing: self.transitionCoordinator))" + + " - self.transitioningDelegate: \(String(describing: self.transitioningDelegate))" + ); + #endif + }; + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated); + + self.lifecycleDelegate? + .viewDidDisappear(sender: self, animated: animated); + + #if DEBUG + print( + "Log - RNIModalViewController.viewDidDisappear" + + " - arg animated: \(animated)" + + " - self.modalNativeID: \(self.modalViewRef?.modalNativeID ?? "N/A")" + + " - self.isBeingDismissed: \(self.isBeingDismissed)" + ); + #endif + }; + + public override func willMove(toParent parent: UIViewController?) { + super.willMove(toParent: parent); + + self.lifecycleDelegate?.willMove(sender: self, toParent: parent); + + #if DEBUG + print( + "Log - RNIModalViewController.willMove" + + " - arg parent == nil: \(parent == nil)" + + " - self.modalNativeID: \(self.modalViewRef?.modalNativeID ?? "N/A")" + + " - self.isMovingFromParent: \(self.isMovingFromParent)" + + " - self.isMovingToParent: \(self.isMovingToParent)" + ); + #endif + }; + + public override func didMove(toParent parent: UIViewController?) { + super.didMove(toParent: parent); + + self.lifecycleDelegate?.didMove(sender: self, toParent: parent); + + #if DEBUG + print( + "Log - RNIModalViewController.willMove" + + " - arg parent == nil: \(parent == nil)" + + " - self.modalNativeID: \(self.modalViewRef?.modalNativeID ?? "N/A")" + + " - self.isMovingFromParent: \(self.isMovingFromParent)" + + " - self.isMovingToParent: \(self.isMovingToParent)" + ); + #endif + }; + + // MARK: - Functions + // ----------------- + + func setPreferredContentSize(withWindow window: UIWindow? = nil){ + guard let computableSize = self.computablePreferredContentSize + else { return }; + + if self.initialSize == nil || self.initialSize!.isZero { + self.initialSize = self.view.bounds.size; + }; + + let targetSize: CGSize = { + let viewSize = self.initialSize!; + let screenSize = window?.bounds.size; + + let size = viewSize.isZero ? screenSize : viewSize; + return size ?? viewSize; + }(); + + switch computableSize.mode { + case .current: + guard let modalContentWrapper = self.modalContentWrapper + else { return }; + + self.preferredContentSize = self.view.systemLayoutSizeFitting( + modalContentWrapper.bounds.size + ); + + default: + self.preferredContentSize = computableSize.compute( + withTargetSize: targetSize, currentSize: .zero + ); + }; + }; + + // MARK: - Private Functions + // ------------------------- + + private func updateBackgroundTransparency(){ + self.view.backgroundColor = { + if self.isBGTransparent { + return .none; + }; + + guard #available(iOS 13, *) else { return .white }; + return .systemBackground; + }(); + + #if DEBUG + print( + "Log - RNIModalViewController.updateBackgroundTransparency" + + " - modalNativeID: '\(self.modalViewRef?.modalNativeID ?? "N/A")'" + + " - self.isBGTransparent: \(self.isBGTransparent)" + ); + #endif + }; + + private func initBackgroundBlur(){ + guard let blurEffectStyle = self.blurEffectStyle else { return }; + + let blurEffect = UIBlurEffect(style: blurEffectStyle); + + let blurEffectView = UIVisualEffectView(effect: blurEffect); + self.blurEffectView = blurEffectView; + + blurEffectView.frame = self.view.bounds; + blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]; + + self.view.insertSubview(blurEffectView, at: 0); + + #if DEBUG + print( + "Log - RNIModalViewController.initBackgroundBlur" + + " - modalNativeID: '\(self.modalViewRef?.modalNativeID ?? "N/A")'" + + " - self.blurEffectStyle: \(blurEffect)" + ); + #endif + }; + + private func removeBackgroundBlur(){ + self.blurEffectView?.removeFromSuperview(); + self.blurEffectView = nil; + }; + + private func updateBackgroundBlur(){ + guard self.isViewLoaded, + let blurEffectStyle = self.blurEffectStyle + else { return }; + + /// bg is transparent, and blur is enabled + let shouldUpdateBackgroundBlur = + self.isBGTransparent && self.isBGBlurred ; + + /// bg is not transparent or blurred + let shouldRemoveBackgroundBlur = + !self.isBGTransparent || !self.isBGBlurred; + + let shouldInitBackgroundBlur = + self.blurEffectView == nil && shouldUpdateBackgroundBlur; + + // 01 - Init background blur first if needed + if shouldInitBackgroundBlur { + self.initBackgroundBlur(); + }; + + if shouldUpdateBackgroundBlur, + let blurEffectView = self.blurEffectView { + + // 02-A - Update background blur + blurEffectView.effect = UIBlurEffect(style: blurEffectStyle); + + #if DEBUG + print( + "Log - RNIModalViewController.updateBackgroundBlur" + + " - modalNativeID: '\(self.modalViewRef?.modalNativeID ?? "N/A")'" + + " - blurEffectStyle: \(blurEffectStyle)" + ); + #endif + + } else if shouldRemoveBackgroundBlur { + // 02-B - background is not transparent or blurred so remove + // background blur + self.removeBackgroundBlur(); + }; + }; +}; diff --git a/ios/React Native/RNIModalView/RNIModalViewError.swift b/ios/React Native/RNIModalView/RNIModalViewError.swift new file mode 100644 index 00000000..0fedb90b --- /dev/null +++ b/ios/React Native/RNIModalView/RNIModalViewError.swift @@ -0,0 +1,35 @@ +// +// RNIModalViewError.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 6/26/20. +// + +import Foundation + +public enum RNIModalViewError: String, CaseIterable { + case modalAlreadyPresented; + case modalAlreadyDismissed; + case modalDismissFailedNotInFocus; + + var errorMessage: String { + RNIModalViewError.getErrorMessage(for: self); + }; + + static func withLabel(_ label: String) -> RNIModalViewError? { + return self.allCases.first{ $0.rawValue == label }; + }; + + static func getErrorMessage(for errorCase: RNIModalViewError) -> String { + switch errorCase { + case .modalAlreadyDismissed: + return "Cannot dismiss modal because it's currently not presented"; + + case .modalAlreadyPresented: + return "Cannot present modal because it's already presented"; + + case .modalDismissFailedNotInFocus: + return "Cannot dismiss modal because it's not in focus. Enable allowModalForceDismiss to dismiss."; + }; + }; +}; diff --git a/ios/React Native/RNIModalView/RNIModalViewManager.m b/ios/React Native/RNIModalView/RNIModalViewManager.m new file mode 100644 index 00000000..c073147e --- /dev/null +++ b/ios/React Native/RNIModalView/RNIModalViewManager.m @@ -0,0 +1,83 @@ + +#import + +@interface RCT_EXTERN_MODULE(RNIModalViewManager, RCTViewManager) + +// MARK: - Props - Callbacks/Events +// -------------------------------- + +RCT_EXPORT_VIEW_PROPERTY(onModalWillPresent, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onModalDidPresent, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalWillDismiss, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onModalDidDismiss, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalWillShow, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onModalDidShow, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalWillHide, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onModalDidHide, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalWillFocus, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onModalDidFocus, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalWillBlur, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onModalDidBlur, RCTBubblingEventBlock) + +RCT_EXPORT_VIEW_PROPERTY(onPresentationControllerWillDismiss, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPresentationControllerDidDismiss, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPresentationControllerDidAttemptToDismiss, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalDetentDidCompute, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onModalDidChangeSelectedDetentIdentifier, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalDidSnap, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalSwipeGestureStart, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onModalSwipeGestureDidEnd, RCTBubblingEventBlock); + +RCT_EXPORT_VIEW_PROPERTY(onModalDismissWillCancel, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onModalDismissDidCancel, RCTBubblingEventBlock); + +// MARK: - Value Props - General +// ----------------------------- + +RCT_EXPORT_VIEW_PROPERTY(modalID, NSString); +RCT_EXPORT_VIEW_PROPERTY(modalContentPreferredContentSize, NSDictionary); + +// MARK: - Value Props - BG-Related +// -------------------------------- + +RCT_EXPORT_VIEW_PROPERTY(isModalBGBlurred, BOOL); +RCT_EXPORT_VIEW_PROPERTY(isModalBGTransparent, BOOL); +RCT_EXPORT_VIEW_PROPERTY(modalBGBlurEffectStyle, NSString); + +// MARK: - Value Props - Presentation/Transition +// --------------------------------------------- + +RCT_EXPORT_VIEW_PROPERTY(modalPresentationStyle, NSString); +RCT_EXPORT_VIEW_PROPERTY(modalTransitionStyle, NSString); + +RCT_EXPORT_VIEW_PROPERTY(hideNonVisibleModals, BOOL); +RCT_EXPORT_VIEW_PROPERTY(presentViaMount, BOOL); +RCT_EXPORT_VIEW_PROPERTY(enableSwipeGesture, BOOL); +RCT_EXPORT_VIEW_PROPERTY(allowModalForceDismiss, BOOL); +RCT_EXPORT_VIEW_PROPERTY(isModalInPresentation, BOOL); + +// MARK: - Value Props - Sheet-Related +// ----------------------------------- + +RCT_EXPORT_VIEW_PROPERTY(modalSheetDetents, NSArray); + +RCT_EXPORT_VIEW_PROPERTY(sheetPrefersScrollingExpandsWhenScrolledToEdge, BOOL); +RCT_EXPORT_VIEW_PROPERTY(sheetPrefersEdgeAttachedInCompactHeight, BOOL); +RCT_EXPORT_VIEW_PROPERTY(sheetWidthFollowsPreferredContentSizeWhenEdgeAttached, BOOL); +RCT_EXPORT_VIEW_PROPERTY(sheetPrefersGrabberVisible, BOOL); +RCT_EXPORT_VIEW_PROPERTY(sheetShouldAnimateChanges, BOOL); + +RCT_EXPORT_VIEW_PROPERTY(sheetLargestUndimmedDetentIdentifier, NSString); +RCT_EXPORT_VIEW_PROPERTY(sheetSelectedDetentIdentifier, NSString); +RCT_EXPORT_VIEW_PROPERTY(sheetPreferredCornerRadius, NSNumber); + +@end + diff --git a/ios/React Native/RNIModalView/RNIModalViewManager.swift b/ios/React Native/RNIModalView/RNIModalViewManager.swift new file mode 100644 index 00000000..b9c79d6d --- /dev/null +++ b/ios/React Native/RNIModalView/RNIModalViewManager.swift @@ -0,0 +1,44 @@ +// +// RNIModalViewManager.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 6/9/20. +// Copyright © 2020 Facebook. All rights reserved. +// + +import Foundation +import React + + +@objc (RNIModalViewManager) +class RNIModalViewManager: RCTViewManager { + static var sharedInstance: RNIModalViewManager!; + + override static func requiresMainQueueSetup() -> Bool { + return true; + }; + + override func view() -> UIView! { + let view = RNIModalView(bridge: self.bridge); + return view; + }; + + override init() { + super.init(); + RNIModalViewManager.sharedInstance = self; + + if !UIViewController.isSwizzled { + UIViewController.swizzleMethods(); + }; + }; + + @objc override func constantsToExport() -> [AnyHashable : Any]! { + return [ + "availableBlurEffectStyles": UIBlurEffect.Style + .availableStyles.map { $0.description }, + + "availablePresentationStyles": UIModalPresentationStyle + .availableStyles.map { $0.description }, + ]; + }; +}; diff --git a/ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapper.swift b/ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapper.swift new file mode 100644 index 00000000..1bc9df37 --- /dev/null +++ b/ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapper.swift @@ -0,0 +1,132 @@ +// +// RNIModalViewControllerWrapper.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/13/23. +// + +import UIKit + +/// Wraps a `UIViewController` so it can be used w/ `RNIModalManager` +/// +public class RNIModalViewControllerWrapper: + RNIIdentifiable, RNIModalIdentifiable, RNIModalPresentationNotifying, + RNIModalState, RNIModalPresentation { + + // MARK: - Properties - RNIIdentifiable + // ------------------------------------ + + public static var synthesizedIdPrefix: String = "modal-wrapper-"; + + // MARK: - Properties + // ------------------ + + /// The view controller that this instance is wrapping/managing + public var viewController: UIViewController; + + // MARK: - Properties - RNIModalPresentationNotifying + // -------------------------------------------------- + + public var modalPresentationNotificationDelegate: RNIModalPresentationNotifiable! + + // MARK: - Properties - RNIModalState + // ---------------------------------- + + public var modalIndexPrev: Int! + public var modalIndex: Int! + + public lazy var modalPresentationState = RNIModalPresentationStateMachine(); + + public var modalFocusState = RNIModalFocusStateMachine(); + + // MARK: - Properties - RNIModalPresentation + // ----------------------------------------- + + /// The modal that is being presented, or to be presented (i.e. + /// `UIViewController.presentedViewController`). + /// + public weak var modalViewController: UIViewController?; + + /// The view controller that presented the modal (i.e. + /// `UIViewController.presentingViewController` + /// + public weak var presentingViewController: UIViewController?; + + public var window: UIWindow? { + self.viewController.view.window ?? + self.presentingViewController?.view.window + }; + + var focusDelegate: RNIModalFocusNotifiable? { + return self.viewController as? RNIModalFocusNotifiable + }; + + // MARK: - Functions + // ----------------- + + public init(viewController: UIViewController){ + self.viewController = viewController; + RNIModalManagerShared.register(modal: self); + }; +}; + +// MARK: - RNIModalRequestable +// --------------------------- + +extension RNIModalViewControllerWrapper: RNIModalRequestable { + + public func requestModalToShow( + sender: Any, + animated: Bool, + completion: @escaping () -> Void + ) throws { + throw RNIModalError( + code: .runtimeError, + message: "presenting is not supported" + ); + }; + + public func requestModalToHide( + sender: Any, + animated: Bool, + completion: @escaping () -> Void + ) throws { + guard let modalVC = self.modalViewController else { + throw RNIModalError( + code: .runtimeError, + message: "Guard check failed, modalViewController is nil" + ); + }; + + modalVC.dismiss(animated: animated) { + completion(); + }; + }; +}; + +// MARK: - RNIModalFocusNotifiable +// ------------------------------- + +extension RNIModalViewControllerWrapper: RNIModalFocusNotifiable { + public func onModalWillFocusNotification(sender: any RNIModal) { + guard let focusDelegate = self.focusDelegate else { return }; + focusDelegate.onModalWillFocusNotification(sender: sender); + }; + + public func onModalDidFocusNotification(sender: any RNIModal) { + guard let focusDelegate = self.focusDelegate else { return }; + focusDelegate.onModalDidFocusNotification(sender: sender); + }; + + public func onModalWillBlurNotification(sender: any RNIModal) { + guard let focusDelegate = self.focusDelegate else { return }; + focusDelegate.onModalWillBlurNotification(sender: sender); + }; + + public func onModalDidBlurNotification(sender: any RNIModal) { + guard let focusDelegate = self.focusDelegate else { return }; + focusDelegate.onModalDidBlurNotification(sender: sender); + }; +}; + + diff --git a/ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapperRegistry.swift b/ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapperRegistry.swift new file mode 100644 index 00000000..cbbb9099 --- /dev/null +++ b/ios/React Native/RNIModalViewControllerWrapper/RNIModalViewControllerWrapperRegistry.swift @@ -0,0 +1,39 @@ +// +// RNIModalViewControllerWrapperRegistry.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +class RNIModalViewControllerWrapperRegistry { + static let instanceMap = NSMapTable< + UIViewController, + RNIModalViewControllerWrapper + >( + keyOptions: .weakMemory, + valueOptions: .weakMemory + ); + + static func get( + forViewController viewController: UIViewController + ) -> RNIModalViewControllerWrapper? { + + Self.instanceMap.object(forKey: viewController); + }; + + static func set( + forViewController viewController: UIViewController, + _ modalWrapper: RNIModalViewControllerWrapper + ){ + + Self.instanceMap.setObject(modalWrapper, forKey: viewController); + }; + + static func isRegistered( + forViewController viewController: UIViewController + ) -> Bool { + Self.get(forViewController: viewController) != nil; + }; +}; diff --git a/ios/React Native/RNIModalViewModule/RNIModalViewModule.m b/ios/React Native/RNIModalViewModule/RNIModalViewModule.m new file mode 100644 index 00000000..e37b151d --- /dev/null +++ b/ios/React Native/RNIModalViewModule/RNIModalViewModule.m @@ -0,0 +1,43 @@ +// +// RNIModalViewModule.m +// RNSwiftReviewer +// +// Created by Dominic Go on 7/11/20. +// + +#import "React/RCTBridgeModule.h" +#import "React/RCTEventEmitter.h" + + +@interface RCT_EXTERN_MODULE(RNIModalViewModule, RCTEventEmitter) + +// MARK: - Standalone Functions +// ---------------------------- + +RCT_EXTERN_METHOD(setModalVisibilityByID: (NSString)modalID + visibility: (BOOL)visibility + animated: (BOOL)visibility + // promise blocks ----------------------- + resolve: (RCTPromiseResolveBlock *)resolve + reject : (RCTPromiseRejectBlock *)reject); + +RCT_EXTERN_METHOD(dismissAllModals: (BOOL)animated + // promise blocks ------------------------ + resolve: (RCTPromiseResolveBlock *)resolve + reject : (RCTPromiseRejectBlock *)reject); + +// MARK: - View-Related Functions +// ------------------------------ + +RCT_EXTERN_METHOD(setModalVisibility: (nonnull NSNumber)node + visibility: (BOOL)visibility + // promise blocks ----------------------- + resolve: (RCTPromiseResolveBlock *)resolve + reject : (RCTPromiseRejectBlock *)reject); + +RCT_EXTERN_METHOD(requestModalInfo: (nonnull NSNumber) node + // promise blocks ----------------------- + resolve: (RCTPromiseResolveBlock *)resolve + reject : (RCTPromiseRejectBlock *)reject); + +@end diff --git a/ios/React Native/RNIModalViewModule/RNIModalViewModule.swift b/ios/React Native/RNIModalViewModule/RNIModalViewModule.swift new file mode 100644 index 00000000..939c11a7 --- /dev/null +++ b/ios/React Native/RNIModalViewModule/RNIModalViewModule.swift @@ -0,0 +1,223 @@ +// +// RNIModalViewModule.swift +// RNSwiftReviewer +// +// Created by Dominic Go on 7/11/20. +// + +import Foundation +import React + +@objc(RNIModalViewModule) +class RNIModalViewModule: RCTEventEmitter { + + enum Events: String, CaseIterable { + case placeholderEvent; + }; + + @objc override static func requiresMainQueueSetup() -> Bool { + return false; + }; + + func getModalViewInstance(for node: NSNumber) -> RNIModalView? { + return RNIUtilities.getView( + forNode: node, + type : RNIModalView.self, + bridge : self.bridge + ); + }; + + // MARK: - Event-Related + // ---------------------- + + private var hasListeners = false; + + override func supportedEvents() -> [String]! { + return Self.Events.allCases.map { $0.rawValue }; + }; + + // called when first listener is added + override func startObserving() { + self.hasListeners = true; + }; + + // called when this module's last listener is removed, or dealloc + override func stopObserving() { + self.hasListeners = false; + }; + + func sendModalEvent(event: Events, params: Dictionary) { + guard self.hasListeners else { return }; + self.sendEvent(withName: event.rawValue, body: params); + }; + + // MARK: - Module Functions + // ------------------------ + + @objc func setModalVisibilityByID( + _ modalID: String, + visibility: Bool, + animated: Bool, + // promise blocks ------------------------ + resolve: @escaping RCTPromiseResolveBlock, + reject : @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.main.async { + let modalInstances = RNIModalManagerShared.modalInstances; + + let debugData: Dictionary = [ + "modalID": modalID, + "visibility": visibility, + ]; + + do { + guard modalInstances.count > 0 else { + throw RNIModalError( + code: .runtimeError, + message: "The list of modalInstances is empty", + debugData: debugData + ); + }; + + let targetModal = modalInstances.first { + $0.modalUserID == modalID || $0.modalNativeID == modalID + }; + + guard let targetModal = targetModal else { + let errorMessage = + "Unable to get the matching RNIModalView instance for" + + " modalID: \(modalID)"; + + throw RNIModalError( + code: .runtimeError, + message: errorMessage, + debugData: debugData + ); + }; + + let modalAction = visibility + ? targetModal.requestModalToShow + : targetModal.requestModalToHide; + + try modalAction(animated, visibility) { + // modal dismissed + resolve([:]); + }; + + } catch let error as RNIModalError { + error.invokePromiseRejectBlock(reject); + + } catch { + var errorWrapper = RNIModalError( + code: .unknownError, + error: error + ); + + errorWrapper.addDebugData(debugData); + errorWrapper.invokePromiseRejectBlock(reject); + }; + }; + }; + + @objc func dismissAllModals( + _ animated: Bool, + // promise blocks ------------------------ + resolve: @escaping RCTPromiseResolveBlock, + reject : @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.main.async { + let windows = RNIUtilities.getWindows(); + let rootViewControllers = windows.map { $0.rootViewController }; + + guard rootViewControllers.isEmpty else { + let error = RNIModalError( + code: .runtimeError, + message: "Could not get root view controllers" + ); + + error.invokePromiseRejectBlock(reject); + return; + }; + + rootViewControllers.enumerated().forEach { + let isLast = $0.offset == rootViewControllers.count - 1; + + $0.element?.dismiss(animated: animated) { + guard isLast else { return }; + resolve([:]); + }; + }; + }; + }; + + // MARK: - View-Related Functions + // ------------------------------ + + @objc func setModalVisibility( + _ node: NSNumber, + visibility: Bool, + // promise blocks ------------------------ + resolve: @escaping RCTPromiseResolveBlock, + reject : @escaping RCTPromiseRejectBlock + ){ + DispatchQueue.main.async { + guard let modalView = self.getModalViewInstance(for: node) else { + let error = RNIModalError( + code: .runtimeError, + message: "Unable to get the matching RNIModalView instance for node", + debugData: [ + "node": node, + "visibility": visibility + ] + ); + + error.invokePromiseRejectBlock(reject); + return; + }; + + let modalAction = visibility + ? modalView.presentModal + : modalView.dismissModal; + + do { + try modalAction(true) { + resolve([:]); + }; + + } catch let error as RNIModalError { + error.invokePromiseRejectBlock(reject) + + } catch { + let errorWrapper = RNIModalError( + code: .unknownError, + error: error + ); + + errorWrapper.invokePromiseRejectBlock(reject); + }; + }; + }; + + @objc func requestModalInfo( + _ node: NSNumber, + // promise blocks ------------------------ + resolve: @escaping RCTPromiseResolveBlock, + reject : @escaping RCTPromiseRejectBlock + ){ + DispatchQueue.main.async { + guard let modalView = self.getModalViewInstance(for: node) else { + let errorMessage = + "Unable to get the corresponding RNIModalView instance" + + " for node: \(node)" + + let error = RNIModalError(code: .runtimeError, message: errorMessage); + error.invokePromiseRejectBlock(reject); + return; + }; + + resolve( + modalView.synthesizedBaseEventData.synthesizedJSDictionary + ); + }; + }; +}; diff --git a/ios/React Native/RNIMulticastDelegate/CAAnimationMulticastDelegate.swift b/ios/React Native/RNIMulticastDelegate/CAAnimationMulticastDelegate.swift new file mode 100644 index 00000000..a4993b9a --- /dev/null +++ b/ios/React Native/RNIMulticastDelegate/CAAnimationMulticastDelegate.swift @@ -0,0 +1,28 @@ +// +// CAAnimationMulticastDelegate.swift +// react-native-ios-modal +// +// Created by Dominic Go on 5/1/23. +// + +import UIKit + + +public class CAAnimationMulticastDelegate: NSObject, CAAnimationDelegate { + + public var emitter: RNIMulticastDelegate = + RNIMulticastDelegate(); + + + public func animationDidStart(_ anim: CAAnimation) { + self.emitter.invoke { + $0.animationDidStart?(anim); + }; + }; + + public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { + self.emitter.invoke { + $0.animationDidStop?(anim, finished: flag); + }; + }; +}; diff --git a/ios/React Native/RNIMulticastDelegate/RNIMulticastDelegate.swift b/ios/React Native/RNIMulticastDelegate/RNIMulticastDelegate.swift new file mode 100644 index 00000000..05a4d1f6 --- /dev/null +++ b/ios/React Native/RNIMulticastDelegate/RNIMulticastDelegate.swift @@ -0,0 +1,27 @@ +// +// MulticastDelegate.swift +// RNSwiftReviewer +// +// Created by Dominic Go on 8/15/20. +// + +import UIKit + + +public class RNIMulticastDelegate { + private let delegates: NSHashTable = NSHashTable.weakObjects(); + + public func add(_ delegate: T) { + delegates.add(delegate); + }; + + public func remove(_ delegate: T) { + self.delegates.remove(delegate); + }; + + public func invoke (_ invocation: @escaping (T) -> Void) { + for delegate in delegates.allObjects { + invocation(delegate) + }; + }; +}; diff --git a/ios/React Native/RNIObjectMetadata/RNIObjectMetadata+Default.swift b/ios/React Native/RNIObjectMetadata/RNIObjectMetadata+Default.swift new file mode 100644 index 00000000..ae8eb150 --- /dev/null +++ b/ios/React Native/RNIObjectMetadata/RNIObjectMetadata+Default.swift @@ -0,0 +1,29 @@ +// +// RNIObjectMetadata+Default.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +fileprivate let RNIObjectMetadataMap = NSMapTable( + keyOptions: .weakMemory, + valueOptions: .strongMemory +); + +public extension RNIObjectMetadata { + var metadata: T? { + set { + if let newValue = newValue { + RNIObjectMetadataMap.setObject(newValue, forKey: self); + + } else { + RNIObjectMetadataMap.removeObject(forKey: self); + }; + } + get { + RNIObjectMetadataMap.object(forKey: self) as? T + } + }; +}; diff --git a/ios/React Native/RNIObjectMetadata/RNIObjectMetadata.swift b/ios/React Native/RNIObjectMetadata/RNIObjectMetadata.swift new file mode 100644 index 00000000..db5040aa --- /dev/null +++ b/ios/React Native/RNIObjectMetadata/RNIObjectMetadata.swift @@ -0,0 +1,14 @@ +// +// RNIObjectMetadata.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/31/23. +// + +import UIKit + +public protocol RNIObjectMetadata: AnyObject { + associatedtype T: AnyObject; + + var metadata: T? { get set }; +}; diff --git a/ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable+Default.swift b/ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable+Default.swift new file mode 100644 index 00000000..f7f8e79c --- /dev/null +++ b/ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable+Default.swift @@ -0,0 +1,42 @@ +// +// RNIViewControllerLifeCycleNotifiable+Default.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/27/23. +// + +import UIKit + +extension RNIViewControllerLifeCycleNotifiable { + public func viewDidLoad(sender: UIViewController) { + // no-op + }; + + public func viewDidLayoutSubviews(sender: UIViewController) { + // no-op + }; + + public func viewWillAppear(sender: UIViewController, animated: Bool) { + // no-op + }; + + public func viewDidAppear(sender: UIViewController, animated: Bool) { + // no-op + }; + + public func viewWillDisappear(sender: UIViewController, animated: Bool) { + // no-op + }; + + public func viewDidDisappear(sender: UIViewController, animated: Bool) { + // no-op + }; + + public func willMove(sender: UIViewController, toParent parent: UIViewController?) { + // no-op + }; + + public func didMove(sender: UIViewController, toParent parent: UIViewController?) { + // no-op + }; +}; diff --git a/ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable.swift b/ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable.swift new file mode 100644 index 00000000..17be9e41 --- /dev/null +++ b/ios/React Native/RNIViewControllerLifeCycleNotifiable/RNIViewControllerLifeCycleNotifiable.swift @@ -0,0 +1,35 @@ +// +// RNIViewControllerLifeCycleNotifiable.swift +// react-native-ios-modal +// +// Created by Dominic Go on 4/1/23. +// + +import UIKit + +public protocol RNIViewControllerLifeCycleNotifiable: AnyObject { + + func viewDidLoad(sender: UIViewController); + + func viewDidLayoutSubviews(sender: UIViewController); + + func viewWillAppear(sender: UIViewController, animated: Bool); + + func viewDidAppear(sender: UIViewController, animated: Bool); + + /// `Note:2023-04-01-14-39-23` + /// + /// * `UIViewController.isBeingDismissed` or + /// `UIViewController.isMovingFromParent` are `true` during + /// `viewWillDisappear`, whenever a modal is about to be dismissed. + /// + func viewWillDisappear(sender: UIViewController, animated: Bool); + + func viewDidDisappear(sender: UIViewController, animated: Bool); + + func willMove(sender: UIViewController, toParent parent: UIViewController?); + + func didMove(sender: UIViewController, toParent parent: UIViewController?); + +}; + diff --git a/ios/React Native/RNIWeak/RNIWeakArray.swift b/ios/React Native/RNIWeak/RNIWeakArray.swift new file mode 100644 index 00000000..9dd4dfe8 --- /dev/null +++ b/ios/React Native/RNIWeak/RNIWeakArray.swift @@ -0,0 +1,64 @@ +// +// RNIWeakArray.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/15/23. +// + +import UIKit + + +public class RNIWeakArray { + + public var rawArray: [RNIWeakRef] = []; + + public var purgedArray: [RNIWeakRef] { + self.rawArray.compactMap { + $0.synthesizedRef == nil ? nil : $0; + }; + }; + + public var array: [T] { + let purgedArray = self.purgedArray; + self.rawArray = purgedArray; + + return purgedArray.compactMap { + $0.synthesizedRef; + }; + }; + + public init(initialItems: [T] = []){ + self.rawArray = initialItems.compactMap { + RNIWeakRef(with: $0) + }; + }; + + public func get(index: Int) -> T? { + guard self.rawArray.count < index else { + return nil + }; + + guard let ref = self.rawArray[index].synthesizedRef else { + self.rawArray.remove(at: index); + return nil; + }; + + return ref; + }; + + public func set(index: Int, element: T) { + guard self.rawArray.count < index else { + return; + }; + + self.rawArray[index] = RNIWeakRef(with: element); + }; + + + public func append(element: T){ + self.rawArray.append( + RNIWeakRef(with: element) + ); + }; +}; + diff --git a/ios/React Native/RNIWeak/RNIWeakDictionary.swift b/ios/React Native/RNIWeak/RNIWeakDictionary.swift new file mode 100644 index 00000000..a445684d --- /dev/null +++ b/ios/React Native/RNIWeak/RNIWeakDictionary.swift @@ -0,0 +1,55 @@ +// +// RNIWeakDictionary.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/15/23. +// + +import UIKit + + +public class RNIWeakDictionary { + + public var rawDict: [K: RNIWeakRef] = [:]; + + public var purgedDict: [K: RNIWeakRef] { + get { + self.rawDict.compactMapValues { + $0.rawRef != nil ? $0 : nil; + } + } + }; + + public var dict: [K: RNIWeakRef] { + get { + let purgedDict = self.purgedDict; + self.rawDict = purgedDict; + + return purgedDict; + } + } + + public func set(for key: K, with value: T){ + self.rawDict[key] = RNIWeakRef(with: value); + }; + + public func get(for key: K) -> T? { + guard let ref = self.rawDict[key]?.synthesizedRef else { + self.rawDict.removeValue(forKey: key); + return nil; + }; + + return ref; + }; + + public subscript(key: K) -> T? { + get { + self.get(for: key); + } + set { + guard let ref = newValue else { return }; + self.set(for: key, with: ref); + } + } +}; + diff --git a/ios/React Native/RNIWeak/RNIWeakRef.swift b/ios/React Native/RNIWeak/RNIWeakRef.swift new file mode 100644 index 00000000..e73462a8 --- /dev/null +++ b/ios/React Native/RNIWeak/RNIWeakRef.swift @@ -0,0 +1,26 @@ +// +// RNIWeakRef.swift +// react-native-ios-modal +// +// Created by Dominic Go on 3/11/23. +// + +import UIKit + + +public class RNIWeakRef { + public weak var rawRef: AnyObject?; + + public var synthesizedRef: T? { + self.rawRef as? T; + }; + + public init(with ref: T) { + self.rawRef = ref as AnyObject; + }; + + public init?(with ref: T?) { + guard let unwrappedRef = ref else { return nil }; + self.rawRef = unwrappedRef as AnyObject; + }; +}; diff --git a/ios/Temp/Extensions+Init/CAGradientLayerType+Init.swift b/ios/Temp/Extensions+Init/CAGradientLayerType+Init.swift new file mode 100644 index 00000000..ea63e8b9 --- /dev/null +++ b/ios/Temp/Extensions+Init/CAGradientLayerType+Init.swift @@ -0,0 +1,27 @@ +// +// CAGradientLayerType+Init.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 4/12/21. +// + +import UIKit + +internal extension CAGradientLayerType { + init?(string: String){ + switch string { + case "axial" : self = .axial; + case "radial": self = .radial; + + case "conic" : + if #available(iOS 12.0, *) { + self = .conic; + + } else { + return nil; + }; + + default: return nil; + } + }; +}; diff --git a/ios/Temp/Extensions+Init/UIImage+Init.swift b/ios/Temp/Extensions+Init/UIImage+Init.swift new file mode 100644 index 00000000..95772b06 --- /dev/null +++ b/ios/Temp/Extensions+Init/UIImage+Init.swift @@ -0,0 +1,56 @@ +// +// UIImage+Init.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 10/2/21. +// + +import UIKit +import UIKit + +internal extension UIImage.RenderingMode { + init?(string: String){ + switch string { + case "automatic" : self = .automatic; + case "alwaysOriginal": self = .alwaysOriginal; + case "alwaysTemplate": self = .alwaysTemplate; + + default: return nil; + }; + }; +}; + +@available(iOS 13.0, *) +internal extension UIImage.SymbolWeight { + init?(string: String){ + switch string { + case "unspecified": self = .unspecified; + case "ultraLight" : self = .ultraLight; + case "thin" : self = .thin; + case "light" : self = .light; + case "regular" : self = .regular; + case "medium" : self = .medium; + case "semibold" : self = .semibold; + case "bold" : self = .bold; + case "heavy" : self = .heavy; + case "black" : self = .black; + + default: return nil; + }; + }; +}; + +@available(iOS 13.0, *) +internal extension UIImage.SymbolScale { + init?(string: String){ + switch string { + case "default" : self = .`default`; + case "unspecified" : self = .unspecified; + case "small" : self = .small; + case "medium" : self = .medium; + case "large" : self = .large; + + default: return nil; + }; + }; +}; diff --git a/ios/Temp/Extensions/UIColor+Helpers.swift b/ios/Temp/Extensions/UIColor+Helpers.swift new file mode 100644 index 00000000..89334241 --- /dev/null +++ b/ios/Temp/Extensions/UIColor+Helpers.swift @@ -0,0 +1,446 @@ +// +// UIColor+Helpers.swift +// IosContextMenuExample +// +// Created by Dominic Go on 11/12/20. +// Copyright © 2020 Facebook. All rights reserved. +// +import UIKit; + + +fileprivate class UIColorHelpers { + struct RGBColor { + var r: CGFloat; + var g: CGFloat; + var b: CGFloat; + + init(r: Int, g: Int, b: Int) { + self.r = CGFloat(r) / 255; + self.g = CGFloat(g) / 255; + self.b = CGFloat(b) / 255; + } + }; + + // css colors strings to UIColor + static let cssColorsToRGB: [String: RGBColor] = [ + // colors red --------------------------------- + "lightsalmon": RGBColor(r: 255, g: 160, b: 122), + "salmon" : RGBColor(r: 250, g: 128, b: 114), + "darksalmon" : RGBColor(r: 233, g: 150, b: 122), + "lightcoral" : RGBColor(r: 240, g: 128, b: 128), + "indianred" : RGBColor(r: 205, g: 92 , b: 92 ), + "crimson" : RGBColor(r: 220, g: 20 , b: 60 ), + "firebrick" : RGBColor(r: 178, g: 34 , b: 34 ), + "red" : RGBColor(r: 255, g: 0 , b: 0 ), + "darkred" : RGBColor(r: 139, g: 0 , b: 0 ), + // colors orange ---------------------------- + "coral" : RGBColor(r: 255, g: 127, b: 80), + "tomato" : RGBColor(r: 255, g: 99 , b: 71), + "orangered" : RGBColor(r: 255, g: 69 , b: 0 ), + "gold" : RGBColor(r: 255, g: 215, b: 0 ), + "orange" : RGBColor(r: 255, g: 165, b: 0 ), + "darkorange": RGBColor(r: 255, g: 140, b: 0 ), + // colors green ---------------------------------------- + "lightyellow" : RGBColor(r: 255, g: 255, b: 224), + "lemonchiffon" : RGBColor(r: 255, g: 250, b: 205), + "lightgoldenrodyellow": RGBColor(r: 250, g: 250, b: 210), + "papayawhip" : RGBColor(r: 255, g: 239, b: 213), + "moccasin" : RGBColor(r: 255, g: 228, b: 181), + "peachpuff" : RGBColor(r: 255, g: 218, b: 185), + "palegoldenrod" : RGBColor(r: 238, g: 232, b: 170), + "khaki" : RGBColor(r: 240, g: 230, b: 140), + "darkkhaki" : RGBColor(r: 189, g: 183, b: 107), + "yellow" : RGBColor(r: 255, g: 255, b: 0 ), + // colors green ------------------------------------- + "lawngreen" : RGBColor(r: 124, g: 252, b: 0 ), + "chartreuse" : RGBColor(r: 127, g: 255, b: 0 ), + "limegreen" : RGBColor(r: 50 , g: 205, b: 50 ), + "lime" : RGBColor(r: 0 , g: 255, b: 0 ), + "forestgreen" : RGBColor(r: 34 , g: 139, b: 34 ), + "green" : RGBColor(r: 0 , g: 128, b: 0 ), + "darkgreen" : RGBColor(r: 0 , g: 100, b: 0 ), + "greenyellow" : RGBColor(r: 173, g: 255, b: 47 ), + "yellowgreen" : RGBColor(r: 154, g: 205, b: 50 ), + "springgreen" : RGBColor(r: 0 , g: 255, b: 127), + "mediumspringgreen": RGBColor(r: 0 , g: 250, b: 154), + "lightgreen" : RGBColor(r: 144, g: 238, b: 144), + "palegreen" : RGBColor(r: 152, g: 251, b: 152), + "darkseagreen" : RGBColor(r: 143, g: 188, b: 143), + "mediumseagreen" : RGBColor(r: 60 , g: 179, b: 113), + "seagreen" : RGBColor(r: 46 , g: 139, b: 87 ), + "olive" : RGBColor(r: 128, g: 128, b: 0 ), + "darkolivegreen" : RGBColor(r: 85 , g: 107, b: 47 ), + "olivedrab" : RGBColor(r: 107, g: 142, b: 35 ), + // colors cyan ------------------------------------- + "lightcyan" : RGBColor(r: 224, g: 255, b: 255), + "cyan" : RGBColor(r: 0 , g: 255, b: 255), + "aqua" : RGBColor(r: 0 , g: 255, b: 255), + "aquamarine" : RGBColor(r: 127, g: 255, b: 212), + "mediumaquamarine": RGBColor(r: 102, g: 205, b: 170), + "paleturquoise" : RGBColor(r: 175, g: 238, b: 238), + "turquoise" : RGBColor(r: 64 , g: 224, b: 208), + "mediumturquoise" : RGBColor(r: 72 , g: 209, b: 204), + "darkturquoise" : RGBColor(r: 0 , g: 206, b: 209), + "lightseagreen" : RGBColor(r: 32 , g: 178, b: 170), + "cadetblue" : RGBColor(r: 95 , g: 158, b: 160), + "darkcyan" : RGBColor(r: 0 , g: 139, b: 139), + "teal" : RGBColor(r: 0 , g: 128, b: 128), + // colors blue ------------------------------------ + "powderblue" : RGBColor(r: 176, g: 224, b: 230), + "lightblue" : RGBColor(r: 173, g: 216, b: 230), + "lightskyblue" : RGBColor(r: 135, g: 206, b: 250), + "skyblue" : RGBColor(r: 135, g: 206, b: 235), + "deepskyblue" : RGBColor(r: 0 , g: 191, b: 255), + "lightsteelblue" : RGBColor(r: 176, g: 196, b: 222), + "dodgerblue" : RGBColor(r: 30 , g: 144, b: 255), + "cornflowerblue" : RGBColor(r: 100, g: 149, b: 237), + "steelblue" : RGBColor(r: 70 , g: 130, b: 180), + "royalblue" : RGBColor(r: 65 , g: 105, b: 225), + "blue" : RGBColor(r: 0 , g: 0 , b: 255), + "mediumblue" : RGBColor(r: 0 , g: 0 , b: 205), + "darkblue" : RGBColor(r: 0 , g: 0 , b: 139), + "navy" : RGBColor(r: 0 , g: 0 , b: 128), + "midnightblue" : RGBColor(r: 25 , g: 25 , b: 112), + "mediumslateblue": RGBColor(r: 123, g: 104, b: 238), + "slateblue" : RGBColor(r: 106, g: 90 , b: 205), + "darkslateblue" : RGBColor(r: 72 , g: 61 , b: 139), + // colors purple ------------------------------- + "lavender" : RGBColor(r: 230, g: 230, b: 250), + "thistle" : RGBColor(r: 216, g: 191, b: 216), + "plum" : RGBColor(r: 221, g: 160, b: 221), + "violet" : RGBColor(r: 238, g: 130, b: 238), + "orchid" : RGBColor(r: 218, g: 112, b: 214), + "fuchsia" : RGBColor(r: 255, g: 0 , b: 255), + "magenta" : RGBColor(r: 255, g: 0 , b: 255), + "mediumorchid": RGBColor(r: 186, g: 85 , b: 211), + "mediumpurple": RGBColor(r: 147, g: 112, b: 219), + "blueviolet" : RGBColor(r: 138, g: 43 , b: 226), + "darkviolet" : RGBColor(r: 148, g: 0 , b: 211), + "darkorchid" : RGBColor(r: 153, g: 50 , b: 204), + "darkmagenta" : RGBColor(r: 139, g: 0 , b: 139), + "purple" : RGBColor(r: 128, g: 0 , b: 128), + "indigo" : RGBColor(r: 75 , g: 0 , b: 130), + // colors pink ------------------------------------ + "pink" : RGBColor(r: 255, g: 192, b: 203), + "lightpink" : RGBColor(r: 255, g: 182, b: 193), + "hotpink" : RGBColor(r: 255, g: 105, b: 180), + "deeppink" : RGBColor(r: 255, g: 20 , b: 147), + "palevioletred" : RGBColor(r: 219, g: 112, b: 147), + "mediumvioletred": RGBColor(r: 199, g: 21 , b: 133), + // colors white --------------------------------- + "white" : RGBColor(r: 255, g: 255, b: 255), + "snow" : RGBColor(r: 255, g: 250, b: 250), + "honeydew" : RGBColor(r: 240, g: 255, b: 240), + "mintcream" : RGBColor(r: 245, g: 255, b: 250), + "azure" : RGBColor(r: 240, g: 255, b: 255), + "aliceblue" : RGBColor(r: 240, g: 248, b: 255), + "ghostwhite" : RGBColor(r: 248, g: 248, b: 255), + "whitesmoke" : RGBColor(r: 245, g: 245, b: 245), + "seashell" : RGBColor(r: 255, g: 245, b: 238), + "beige" : RGBColor(r: 245, g: 245, b: 220), + "oldlace" : RGBColor(r: 253, g: 245, b: 230), + "floralwhite" : RGBColor(r: 255, g: 250, b: 240), + "ivory" : RGBColor(r: 255, g: 255, b: 240), + "antiquewhite" : RGBColor(r: 250, g: 235, b: 215), + "linen" : RGBColor(r: 250, g: 240, b: 230), + "lavenderblush": RGBColor(r: 255, g: 240, b: 245), + "mistyrose" : RGBColor(r: 255, g: 228, b: 225), + // colors gray ----------------------------------- + "gainsboro" : RGBColor(r: 220, g: 220, b: 220), + "lightgray" : RGBColor(r: 211, g: 211, b: 211), + "silver" : RGBColor(r: 192, g: 192, b: 192), + "darkgray" : RGBColor(r: 169, g: 169, b: 169), + "gray" : RGBColor(r: 128, g: 128, b: 128), + "dimgray" : RGBColor(r: 105, g: 105, b: 105), + "lightslategray": RGBColor(r: 119, g: 136, b: 153), + "slategray" : RGBColor(r: 112, g: 128, b: 144), + "darkslategray" : RGBColor(r: 47 , g: 79 , b: 79 ), + "black" : RGBColor(r: 0 , g: 0 , b: 0 ), + // colors brown ---------------------------------- + "cornsilk" : RGBColor(r: 255, g: 248, b: 220), + "blanchedalmond": RGBColor(r: 255, g: 235, b: 205), + "bisque" : RGBColor(r: 255, g: 228, b: 196), + "navajowhite" : RGBColor(r: 255, g: 222, b: 173), + "wheat" : RGBColor(r: 245, g: 222, b: 179), + "burlywood" : RGBColor(r: 222, g: 184, b: 135), + "tan" : RGBColor(r: 210, g: 180, b: 140), + "rosybrown" : RGBColor(r: 188, g: 143, b: 143), + "sandybrown" : RGBColor(r: 244, g: 164, b: 96 ), + "goldenrod" : RGBColor(r: 218, g: 165, b: 32 ), + "peru" : RGBColor(r: 205, g: 133, b: 63 ), + "chocolate" : RGBColor(r: 210, g: 105, b: 30 ), + "saddlebrown" : RGBColor(r: 139, g: 69 , b: 19 ), + "sienna" : RGBColor(r: 160, g: 82 , b: 45 ), + "brown" : RGBColor(r: 165, g: 42 , b: 42 ), + "maroon" : RGBColor(r: 128, g: 0 , b: 0 ) + ]; + + static func normalizeHexString(_ hex: String?) -> String { + guard var hexString = hex else { + return "00000000"; + }; + + if hexString.hasPrefix("#") { + hexString = String(hexString.dropFirst()); + }; + + if hexString.count == 3 || hexString.count == 4 { + hexString = hexString.map { "\($0)\($0)" }.joined(); + }; + + let hasAlpha = hexString.count > 7; + if !hasAlpha { + hexString += "ff"; + }; + + return hexString; + }; +}; + +internal extension UIColor { + + var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + var red : CGFloat = 0; + var green: CGFloat = 0; + var blue : CGFloat = 0; + var alpha: CGFloat = 0; + + getRed(&red, green: &green, blue: &blue, alpha: &alpha); + return (red, green, blue, alpha); + }; + + @available(iOS 13.0, *) + static func elementColorFromString(_ string: String) -> UIColor? { + switch string { + // Label Colors + case "label": return .label; + case "secondaryLabel": return .secondaryLabel; + case "tertiaryLabel": return .tertiaryLabel; + case "quaternaryLabel": return .quaternaryLabel; + + // Fill Colors + case "systemFill": return .systemFill; + case "secondarySystemFill": return .secondarySystemFill; + case "tertiarySystemFill": return .tertiarySystemFill; + case "quaternarySystemFill": return .quaternarySystemFill; + + // Text Colors + case "placeholderText": return .placeholderText; + + // Standard Content Background Colors + case "systemBackground": return .systemBackground; + case "secondarySystemBackground": return .secondarySystemBackground; + case "tertiarySystemBackground": return .tertiarySystemBackground; + + // Grouped Content Background Colors + case "systemGroupedBackground": return .systemGroupedBackground; + case "secondarySystemGroupedBackground": return .secondarySystemGroupedBackground; + case "tertiarySystemGroupedBackground": return .tertiarySystemGroupedBackground; + + // Separator Colors + case "separator": return .separator; + case "opaqueSeparator": return .opaqueSeparator; + + // Link Color + case "link": return .link; + + // Non-adaptable Colors + case "darkText": return .darkText; + case "lightText": return .lightText; + + default: return nil; + }; + }; + + static func systemColorFromString(_ string: String) -> UIColor? { + switch string { + // Adaptable Colors + case "systemBlue" : return .systemBlue; + case "systemGreen" : return .systemGreen; + case "systemOrange": return .systemOrange; + case "systemPink" : return .systemPink; + case "systemPurple": return .systemPurple; + case "systemRed" : return .systemRed; + case "systemTeal" : return .systemTeal; + case "systemYellow": return .systemYellow; + + default: break; + }; + + if #available(iOS 13.0, *) { + switch string { + case "systemIndigo" : return .systemIndigo; + + //Adaptable Gray Colors + case "systemGray" : return .systemGray; + case "systemGray2": return .systemGray2; + case "systemGray3": return .systemGray3; + case "systemGray4": return .systemGray4; + case "systemGray5": return .systemGray5; + case "systemGray6": return .systemGray6; + + default: break; + }; + }; + + return nil; + }; + + /// Parse "react native" color to `UIColor` + /// Swift impl. `RCTConvert` color + static func parseColor(value: Any) -> UIColor? { + if let string = value as? String { + if #available(iOS 13.0, *), + let color = Self.elementColorFromString(string) { + + // a: iOS 13+ ui enum colors + return color; + + } else if let color = Self.systemColorFromString(string) { + // b: iOS system enum colors + return color; + + } else { + // c: react-native color string + return UIColor(cssColor: string); + }; + + } else if let dict = value as? NSDictionary { + // d: react-native DynamicColor object + return UIColor(dynamicDict: dict); + }; + + return nil; + }; + + /// create color from css color code string + convenience init?(cssColorCode: String) { + guard let color = UIColorHelpers.cssColorsToRGB[cssColorCode.lowercased()] + else { return nil }; + + self.init(red: color.r, green: color.g, blue: color.b, alpha: 1); + }; + + /// create color from hex color string + convenience init?(hexString: String) { + guard hexString.hasPrefix("#") else { return nil }; + let hexColor: String = UIColorHelpers.normalizeHexString(hexString); + + // invalid hex string + guard hexColor.count == 8 else { return nil }; + + var hexNumber: UInt64 = 0; + let scanner = Scanner(string: hexColor); + + // failed to convert hex string + guard scanner.scanHexInt64(&hexNumber) else { return nil }; + + self.init( + red : CGFloat((hexNumber & 0xff000000) >> 24) / 255, + green: CGFloat((hexNumber & 0x00ff0000) >> 16) / 255, + blue : CGFloat((hexNumber & 0x0000ff00) >> 8 ) / 255, + alpha: CGFloat( hexNumber & 0x000000ff) / 255 + ); + }; + + /// create color from rgb/rgba string + convenience init?(rgbString: String){ + // create mutable copy... + var rgbString = rgbString; + + // check if rgba() string + let hasAlpha = rgbString.hasPrefix("rgba"); + + // remove "rgb(" or "rgba" prefix + rgbString = rgbString.replacingOccurrences( + of: hasAlpha ? "rgba(" : "rgb(", + with: "", + options: [.caseInsensitive] + ); + + // remove ")" suffix + rgbString = rgbString.replacingOccurrences( + of: ")", with: "", options: [.backwards] + ); + + // split up the rgb values seperated by "," + let split = rgbString.components(separatedBy: ","); + + // convert to array of float + let colors = split.compactMap { + NumberFormatter().number(from: $0) as? CGFloat; + }; + + if(colors.count == 3) { + // create UIColor from rgb(...) string + self.init( + red : colors[0] / 255, + green: colors[1] / 255, + blue : colors[2] / 255, + alpha: 1 + ); + + } else if(colors.count == 4) { + // create UIColor from rgba(...) string + self.init( + red : colors[0] / 255, + green: colors[1] / 255, + blue : colors[2] / 255, + alpha: colors[3] + ); + + } else { + // invalid rgb color string + // color array is < 3 or > 4 + return nil; + }; + }; + + /// create color from rgb/rgba/hex/csscolor strings + convenience init?(cssColor: String){ + // remove whitespace characters + let colorString = cssColor.trimmingCharacters(in: .whitespacesAndNewlines); + + if colorString.hasPrefix("#"){ + self.init(hexString: colorString); + return; + + } else if colorString.hasPrefix("rgb") { + self.init(rgbString: colorString); + + } else if let color = UIColorHelpers.cssColorsToRGB[colorString.lowercased()] { + self.init(red: color.r, green: color.g, blue: color.b, alpha: 1); + return; + + } else { + return nil; + }; + }; + + /// create color from `DynamicColorIOS` dictionary + convenience init?(dynamicDict: NSDictionary) { + guard let dict = dynamicDict["dynamic"] as? NSDictionary, + let stringDark = dict["dark" ] as? String, + let stringLight = dict["light"] as? String + else { return nil }; + + if #available(iOS 13.0, *), + let colorDark = UIColor(cssColor: stringDark ), + let colorLight = UIColor(cssColor: stringLight) { + + self.init(dynamicProvider: { traitCollection in + switch traitCollection.userInterfaceStyle { + case .dark : return colorDark; + case .light: return colorLight; + + case .unspecified: fallthrough; + @unknown default : return .clear; + }; + }); + + } else { + self.init(cssColor: stringLight); + }; + }; + +}; diff --git a/ios/Temp/Extensions/UIViewController+Helpers.swift b/ios/Temp/Extensions/UIViewController+Helpers.swift new file mode 100644 index 00000000..16598e75 --- /dev/null +++ b/ios/Temp/Extensions/UIViewController+Helpers.swift @@ -0,0 +1,24 @@ +// +// UIViewController+Helpers.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 8/26/22. +// + +import UIKit + +internal extension UIViewController { + func attachChildVC(_ child: UIViewController) { + self.addChild(child); + self.view.addSubview(child.view); + child.didMove(toParent: self); + }; + + func detachFromParentVC() { + guard self.parent != nil else { return }; + + self.willMove(toParent: nil); + self.view.removeFromSuperview(); + self.removeFromParent(); + }; +}; diff --git a/ios/Temp/IosUtilities-Bridging-Header.h b/ios/Temp/IosUtilities-Bridging-Header.h new file mode 100644 index 00000000..2ab2b743 --- /dev/null +++ b/ios/Temp/IosUtilities-Bridging-Header.h @@ -0,0 +1,16 @@ +#if DEBUG +#import +#endif + +#import +#import + +#import +#import + +#import +#import + +#import +#import +#import diff --git a/ios/Temp/Protocols/RNIJSComponentWillUnmountNotifiable.swift b/ios/Temp/Protocols/RNIJSComponentWillUnmountNotifiable.swift new file mode 100644 index 00000000..946c4415 --- /dev/null +++ b/ios/Temp/Protocols/RNIJSComponentWillUnmountNotifiable.swift @@ -0,0 +1,18 @@ +// +// RNIJSComponentWillUnmountNotifiable.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/25/22. +// + +import UIKit + + +/// +/// When a class implements this protocol, it means that it receives a notification from JS-side whenever +/// the component's `componentWillUnmount` lifecycle is triggered. +internal protocol RNIJSComponentWillUnmountNotifiable { + + func notifyOnJSComponentWillUnmount(); + +}; diff --git a/ios/Temp/RNICleanup/RNICleanable.swift b/ios/Temp/RNICleanup/RNICleanable.swift new file mode 100644 index 00000000..0a0f1243 --- /dev/null +++ b/ios/Temp/RNICleanup/RNICleanable.swift @@ -0,0 +1,17 @@ +// +// RNICleanable.swift +// react-native-ios-popover +// +// Created by Dominic Go on 3/13/22. +// + +import UIKit + +/// +/// When a class implements this protocol, it means that the class has "clean-up" related code. +/// This is usually for `UIView` subclasses, and a +internal protocol RNICleanable: AnyObject { + + func cleanup(); + +}; diff --git a/ios/Temp/RNICleanup/RNICleanupMode.swift b/ios/Temp/RNICleanup/RNICleanupMode.swift new file mode 100644 index 00000000..eef47e01 --- /dev/null +++ b/ios/Temp/RNICleanup/RNICleanupMode.swift @@ -0,0 +1,25 @@ +// +// RNICleanupMode.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/20/22. +// + +import UIKit + + +/// If a class conforms to `RNICleanable`, this enum determines how the cleanup routine is triggered. +internal enum RNICleanupMode: String { + + case automatic; + + /// Trigger cleanup via view controller lifecycle + case viewController; + + /// Trigger cleanup via react lifecycle `componentWillUnmount` event sent from js + /// I.e. via `RNIJSComponentWillUnmountNotifiable` + case reactComponentWillUnmount; + + case disabled; + +}; diff --git a/ios/Temp/RNICleanup/RNIInternalCleanupMode.swift b/ios/Temp/RNICleanup/RNIInternalCleanupMode.swift new file mode 100644 index 00000000..a238bcd9 --- /dev/null +++ b/ios/Temp/RNICleanup/RNIInternalCleanupMode.swift @@ -0,0 +1,40 @@ +// +// RNIInternalCleanupMode.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 10/29/22. +// + +import UIKit + + +internal protocol RNIInternalCleanupMode { + /// shadow variable for react prop + var synthesizedInternalCleanupMode: RNICleanupMode { get }; + + /// exported react prop + var internalCleanupMode: String? { get }; + + /// computed property - override behavior for `.automatic` + var cleanupMode: RNICleanupMode { get }; +}; + +// provide default implementation +internal extension RNIInternalCleanupMode { + var shouldEnableAttachToParentVC: Bool { + self.cleanupMode == .viewController + }; + + var shouldEnableCleanup: Bool { + self.cleanupMode != .disabled + }; + + var cleanupMode: RNICleanupMode { + get { + switch self.synthesizedInternalCleanupMode { + case .automatic: return .reactComponentWillUnmount; + default: return self.synthesizedInternalCleanupMode; + }; + } + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageGradientMaker.swift b/ios/Temp/RNIImage/RNIImageGradientMaker.swift new file mode 100644 index 00000000..f04245e2 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageGradientMaker.swift @@ -0,0 +1,181 @@ +// +// RNIImageGradientMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import UIKit +import UIKit + + +internal struct RNIImageGradientMaker { + internal enum PointPresets: String { + case top, bottom, left, right; + case bottomLeft, bottomRight, topLeft, topRight; + + internal var cgPoint: CGPoint { + switch self { + case .top : return CGPoint(x: 0.5, y: 0.0); + case .bottom: return CGPoint(x: 0.5, y: 1.0); + + case .left : return CGPoint(x: 0.0, y: 0.5); + case .right: return CGPoint(x: 1.0, y: 0.5); + + case .bottomLeft : return CGPoint(x: 0.0, y: 1.0); + case .bottomRight: return CGPoint(x: 1.0, y: 1.0); + + case .topLeft : return CGPoint(x: 0.0, y: 0.0); + case .topRight: return CGPoint(x: 1.0, y: 0.0); + }; + }; + }; + + internal enum DirectionPresets: String { + // horizontal + case leftToRight, rightToLeft; + // vertical + case topToBottom, bottomToTop; + // diagonal + case topLeftToBottomRight, topRightToBottomLeft; + case bottomLeftToTopRight, bottomRightToTopLeft; + + internal var point: (start: CGPoint, end: CGPoint) { + switch self { + case .leftToRight: + return (CGPoint(x: 0.0, y: 0.5), CGPoint(x: 1.0, y: 1.5)); + + case .rightToLeft: + return (CGPoint(x: 1.0, y: 0.5), CGPoint(x: 0.0, y: 0.5)); + + case .topToBottom: + return (CGPoint(x: 0.5, y: 1.0), CGPoint(x: 0.5, y: 0.0)); + + case .bottomToTop: + return (CGPoint(x: 0.5, y: 1.0), CGPoint(x: 0.5, y: 0.0)); + + case .topLeftToBottomRight: + return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y: 1.0)); + + case .topRightToBottomLeft: + return (CGPoint(x: 1.0, y: 0.0), CGPoint(x: 0.0, y: 1.0)); + + case .bottomLeftToTopRight: + return (CGPoint(x: 0.0, y: 1.0), CGPoint(x: 1.0, y: 0.0)); + + case .bottomRightToTopLeft: + return (CGPoint(x: 1.0, y: 1.0), CGPoint(x: 0.0, y: 0.0)); + }; + }; + }; + + static private func extractCGPoint(dict: NSDictionary) -> CGPoint? { + guard let x = dict["x"] as? CGFloat, + let y = dict["y"] as? CGFloat + else { return nil }; + + return CGPoint(x: x, y: y); + }; + + static private func extractPoint(dict: NSDictionary, key: String) -> CGPoint? { + if let pointDict = dict[key] as? NSDictionary, + let point = Self.extractCGPoint(dict: pointDict) { + + return point; + + } else if let pointString = dict[key] as? String, + let point = PointPresets(rawValue: pointString) { + + return point.cgPoint; + + } else { + return nil; + }; + } + + internal let type: CAGradientLayerType; + + internal let colors : [CGColor]; + internal let locations : [NSNumber]?; + internal let startPoint: CGPoint; + internal let endPoint : CGPoint; + + internal var size: CGSize; + internal let borderRadius: CGFloat; + + internal var gradientLayer: CALayer { + let layer = CAGradientLayer(); + + layer.type = self.type; + layer.colors = self.colors; + layer.locations = self.locations; + layer.startPoint = self.startPoint; + layer.endPoint = self.endPoint; + layer.cornerRadius = self.borderRadius; + + return layer; + }; + + internal init?(dict: NSDictionary) { + guard let colors = dict["colors"] as? NSArray + else { return nil }; + + self.colors = colors.compactMap { + guard let string = $0 as? String, + let color = UIColor(cssColor: string) + else { return nil }; + + return color.cgColor; + }; + + self.type = { + guard let string = dict["type"] as? String, + let type = CAGradientLayerType(string: string) + else { return .axial }; + + return type; + }(); + + self.locations = { + guard let locations = dict["locations"] as? NSArray else { return nil }; + return locations.compactMap { $0 as? NSNumber }; + }(); + + self.startPoint = Self.extractPoint(dict: dict, key: "startPoint") + ?? PointPresets.top.cgPoint; + + self.endPoint = Self.extractPoint(dict: dict, key: "endPoint") + ?? PointPresets.bottom.cgPoint; + + self.size = CGSize( + width : (dict["width" ] as? CGFloat) ?? 0, + height: (dict["height"] as? CGFloat) ?? 0 + ); + + self.borderRadius = dict["borderRadius"] as? CGFloat ?? 0; + }; + + internal mutating func setSizeIfNotSet(_ newSize: CGSize){ + self.size = CGSize( + width : self.size.width <= 0 ? newSize.width : self.size.width, + height: self.size.height <= 0 ? newSize.height : self.size.height + ); + }; + + internal func makeImage() -> UIImage { + return UIGraphicsImageRenderer(size: self.size).image { context in + let rect = CGRect(origin: .zero, size: self.size); + + let gradient = self.gradientLayer; + gradient.frame = rect; + gradient.render(in: context.cgContext); + + let clipPath = UIBezierPath( + roundedRect : rect, + cornerRadius: self.borderRadius + ); + + clipPath.addClip(); + }; + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageItem.swift b/ios/Temp/RNIImage/RNIImageItem.swift new file mode 100644 index 00000000..0e500540 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageItem.swift @@ -0,0 +1,212 @@ +// +// RNIImageItem.swift +// IosNavigatorExample +// +// Created by Dominic Go on 1/29/21. +// + +import UIKit + + +internal class RNIImageItem { + + // MARK: - Properties - Config + // --------------------------- + + internal let type: RNIImageType; + internal let imageValue: Any?; + internal let imageOptions: RNIImageOptions; + + internal var defaultSize: CGSize; + + // MARK: Properties - Misc + // ----------------------- + + internal let imageConfig: RNIImageConfig; + internal var loadedImage: UIImage?; + + // MARK: Properties - Computed + // --------------------------- + + internal var baseImage: UIImage? { + switch self.imageConfig { + case let .IMAGE_ASSET(assetName): + return UIImage(named: assetName); + + case let .IMAGE_SYSTEM(imageConfig): + guard #available(iOS 13.0, *) else { return nil }; + return imageConfig.image; + + case let .IMAGE_REQUIRE(imageConfig): + return imageConfig.image; + + case .IMAGE_EMPTY: + return UIImage(); + + case let .IMAGE_RECT(imageConfig): + return imageConfig.makeImage(); + + case let .IMAGE_GRADIENT(imageConfig): + return imageConfig.makeImage(); + + case let .IMAGE_REMOTE_URL(imageConfig): + return imageConfig.image; + }; + }; + + internal var imageWithTint: UIImage? { + guard var image = self.baseImage else { return nil }; + guard let tint = self.imageOptions.tint else { return image }; + + let isTintTransparent = tint.rgba.a < 1; + + if isTintTransparent { + let overlay = RNIImageMaker(size: image.size, fillColor: tint, borderRadius: 0); + let overlayImage = overlay.makeImage(); + + return UIGraphicsImageRenderer(size: image.size).image { context in + let rect = CGRect(origin: .zero, size: image.size); + + image.draw(in: rect); + overlayImage.draw(in: rect); + }; + }; + + if image.renderingMode != self.imageOptions.renderingMode { + image = image.withRenderingMode(self.imageOptions.renderingMode) + }; + + if #available(iOS 13.0, *) { + image = image.withTintColor( + tint, + renderingMode: self.imageOptions.renderingMode + ); + }; + + return image; + }; + + internal var imageWithRoundedEdges: UIImage? { + guard let image = self.imageWithTint + else { return nil }; + + guard let cornerRadius = self.imageOptions.cornerRadius + else { return image }; + + return UIGraphicsImageRenderer(size: image.size).image { context in + let rect = CGRect(origin: .zero, size: image.size); + + let clipPath = UIBezierPath( + roundedRect : rect, + cornerRadius: cornerRadius + ); + + clipPath.addClip(); + image.draw(in: rect); + }; + }; + + internal var image: UIImage? { + self.imageWithRoundedEdges + }; + + internal var dictionary: [String: Any] { + var dict: [String: Any] = [ + "type": self.type + ]; + + if let imageValue = self.imageValue { + dict["imageValue"] = imageValue; + }; + + return dict; + }; + + // MARK: - Init + // ----------- + + internal init?( + type: RNIImageType, + imageValue: Any?, + imageOptions: NSDictionary?, + imageLoadingConfig: NSDictionary? = nil, + defaultImageSize: CGSize = CGSize(width: 100, height: 100) + ){ + + self.type = type; + self.imageValue = imageValue; + self.defaultSize = defaultImageSize; + + guard let imageConfig: RNIImageConfig = { + switch type { + case .IMAGE_ASSET: + guard let string = imageValue as? String + else { return nil }; + + return .IMAGE_ASSET(assetName: string); + + case .IMAGE_SYSTEM: + guard #available(iOS 13.0, *), + let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageSystemMaker(dict: rawConfig) + else { return nil }; + + return .IMAGE_SYSTEM(config: imageConfig); + + case .IMAGE_REQUIRE: + guard let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageRequireMaker( + dict: rawConfig, + imageLoadingConfig: imageLoadingConfig + ) + else { return nil }; + + return .IMAGE_REQUIRE(config: imageConfig); + + case .IMAGE_EMPTY: + return .IMAGE_EMPTY; + + case .IMAGE_RECT: + guard let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageMaker(dict: rawConfig) + else { return nil }; + + return .IMAGE_RECT(config: imageConfig); + + case .IMAGE_GRADIENT: + guard let rawConfig = imageValue as? NSDictionary, + var imageConfig = RNIImageGradientMaker(dict: rawConfig) + else { return nil }; + + imageConfig.setSizeIfNotSet(defaultImageSize); + return .IMAGE_GRADIENT(config: imageConfig); + + case .IMAGE_REMOTE_URL: + guard let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageRemoteURLMaker( + dict: rawConfig, + imageLoadingConfig: imageLoadingConfig + ) + else { return nil }; + + return .IMAGE_REMOTE_URL(config: imageConfig); + }; + }() else { return nil }; + + self.imageConfig = imageConfig; + self.imageOptions = RNIImageOptions(dict: imageOptions ?? [:]); + }; + + internal convenience init?(dict: NSDictionary){ + guard let typeString = dict["type"] as? String, + let type = RNIImageType(rawValue: typeString) + else { return nil }; + + self.init( + type: type, + imageValue: dict["imageValue"], + imageOptions: dict["imageOptions"] as? NSDictionary, + imageLoadingConfig: dict["imageLoadingConfig"] as? NSDictionary + ); + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageLoadingConfig.swift b/ios/Temp/RNIImage/RNIImageLoadingConfig.swift new file mode 100644 index 00000000..4b4cde62 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageLoadingConfig.swift @@ -0,0 +1,26 @@ +// +// RNIImageCacheAndLoadingConfig.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +import UIKit + + +internal protocol RNIImageLoadingConfigurable { + var shouldCache: Bool? { get }; + var shouldLazyLoad: Bool? { get }; +}; + +// TODO: Per file defaults via extension +internal struct RNIImageLoadingConfig: RNIImageLoadingConfigurable { + + internal let shouldCache: Bool?; + internal let shouldLazyLoad: Bool?; + + internal init(dict: NSDictionary) { + self.shouldCache = dict["shouldCache"] as? Bool; + self.shouldLazyLoad = dict["shouldLazyLoad"] as? Bool; + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageMaker.swift b/ios/Temp/RNIImage/RNIImageMaker.swift new file mode 100644 index 00000000..f96e415a --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageMaker.swift @@ -0,0 +1,59 @@ +// +// RNIImageMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import UIKit +import UIKit + + +internal struct RNIImageMaker { + + internal let size : CGSize; + internal let fillColor : UIColor; + internal let borderRadius: CGFloat; + + internal init( + size: CGSize, + fillColor: UIColor, + borderRadius: CGFloat + ) { + self.size = size; + self.fillColor = fillColor; + self.borderRadius = borderRadius; + }; + + internal init?(dict: NSDictionary) { + guard let width = dict["width" ] as? CGFloat, + let height = dict["height"] as? CGFloat + else { return nil }; + + self.size = CGSize(width: width, height: height); + + guard let fillColorValue = dict["fillColor" ], + let fillColor = UIColor.parseColor(value: fillColorValue) + else { return nil }; + + self.fillColor = fillColor; + + self.borderRadius = dict["borderRadius"] as? CGFloat ?? 0; + }; + + internal func makeImage() -> UIImage { + return UIGraphicsImageRenderer(size: self.size).image { context in + let rect = CGRect(origin: .zero, size: self.size); + + let clipPath = UIBezierPath( + roundedRect : rect, + cornerRadius: self.borderRadius + ); + + clipPath.addClip(); + self.fillColor.setFill(); + + context.fill(rect); + }; + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageOptions.swift b/ios/Temp/RNIImage/RNIImageOptions.swift new file mode 100644 index 00000000..deb7d4c5 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageOptions.swift @@ -0,0 +1,35 @@ +// +// RNIImageOptions.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/28/22. +// + +import UIKit + + +internal struct RNIImageOptions { + let tint: UIColor?; + let renderingMode: UIImage.RenderingMode; + let cornerRadius: CGFloat?; + + init(dict: NSDictionary){ + self.tint = { + guard let value = dict["tint"], + let color = UIColor.parseColor(value: value) + else { return nil }; + + return color; + }(); + + self.renderingMode = { + guard let string = dict["renderingMode"] as? String, + let mode = UIImage.RenderingMode(string: string) + else { return .automatic }; + + return mode; + }(); + + self.cornerRadius = dict["cornerRadius"] as? CGFloat; + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageRemoteURLMaker.swift b/ios/Temp/RNIImage/RNIImageRemoteURLMaker.swift new file mode 100644 index 00000000..bd1ebe15 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageRemoteURLMaker.swift @@ -0,0 +1,361 @@ +// +// RNIImageRemoteURLMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +import UIKit +import React + + +internal class RNIImageRemoteURLMaker { + + // MARK: - Embedded Types + // ---------------------- + + /// Note: Unable to synthesize `Equatable` conformance because of `Error` associated value. + internal enum State { + + // MARK: - Start State + // ------------------- + + case INITIAL; + + // MARK: - Intermediate State + // -------------------------- + + case LOADING; + + // loading was triggered again because it failed previously + case RETRYING(prevError: Error); + + case LOADED_ERROR (error: Error? = nil); + + // MARK: - Final State + // ------------------- + + case LOADED(image: UIImage); + + // no more remaining retry attempts, don't trigger loading anymore + case LOADED_ERROR_FINAL (error: Error? = nil); + + // MARK: - Computed Properties + // --------------------------- + + var isLoading: Bool { + switch self { + case .LOADING : fallthrough; + case .RETRYING: return true; + + default: return false; + }; + }; + + var error: Error? { + switch self { + case let .LOADED_ERROR(error): return error; + default: return nil; + }; + }; + + var isErrorState: Bool { + switch self { + case .LOADED_ERROR: return true; + default: return false; + }; + }; + + var isFinalState: Bool { + switch self { + case .LOADED : fallthrough; + case .LOADED_ERROR_FINAL: return true; + + default: return false; + }; + }; + }; + + internal typealias ImageDidLoadHandler = ( + _ isSuccess: Bool, + _ sender: RNIImageRemoteURLMaker + ) -> Void; + + // MARK: - Class Members + // --------------------- + + internal static var imageCache: [String: UIImage] = [:]; + + // MARK: - Properties - Serialized + // ------------------------------- + + internal let urlString: String; + + // MARK: - Properties - Derived/Parsed + // ----------------------------------- + + internal let url: URL?; + internal let imageLoadingConfig: RNIRemoteURLImageLoadingConfig; + + internal let fallbackImageConfig: RNIImageItem?; + + // MARK: - Properties + // ------------------ + + internal lazy var imageLoader: RCTImageLoaderWithAttributionProtocol? = { + RNIUtilities.sharedBridge?.module(forName: "ImageLoader") as? + RCTImageLoaderWithAttributionProtocol; + }(); + + internal var state: State = .INITIAL; + internal var loadingAttemptsCount = 0; + + /// Reminder: Use weak self to prevent retain cycle + memory leak + internal var onImageDidLoadBlock: ImageDidLoadHandler?; + + // MARK: - Properties - Computed + // ----------------------------- + + private var cachedImage: UIImage? { + guard self.imageLoadingConfig._shouldCache, + let cachedImage = Self.imageCache[self.urlString] + else { return nil }; + + return cachedImage; + }; + + internal var shouldRetry: Bool { + let maxRetryAttempts = self.imageLoadingConfig._maxRetryAttempts; + + // Note: negative max retry attempt means infinite retry + return maxRetryAttempts < 0 + ? true + : self.loadingAttemptsCount < maxRetryAttempts; + }; + + // Get image w/o triggering loading logic (i.e. no side effects) + // This will also use the fallback image when appropriate + internal var _image: UIImage? { + let fallbackBehavior = self.imageLoadingConfig._fallbackBehavior; + + switch self.state { + case .INITIAL: fallthrough; + case .LOADING: + // A - Use fallback image when the remote image hasn't been loaded yet + switch fallbackBehavior { + case .whileNotLoaded: return self.fallbackImage; + default: return nil; + }; + + case .RETRYING : fallthrough; + case .LOADED_ERROR: + // B - Use fallback image when the remote image hasn't been loaded yet + // due to an error + switch fallbackBehavior { + case .whileNotLoaded: fallthrough; + case .onLoadError : return self.fallbackImage; + + default: return nil; + }; + + case .LOADED_ERROR_FINAL: + // C - Use fallback image when the remote image has failed to load, and + // no more "retry loading" attempts remaining + switch fallbackBehavior { + case .whileNotLoaded : fallthrough; + case .afterFinalAttempt: fallthrough; + case .onLoadError : return self.fallbackImage; + }; + + case .LOADED(image: let image): + return image; + }; + }; + + // Get image + trigger loading logic when not yet loaded + internal var image: UIImage? { + switch self.state { + case .INITIAL: + // A - image not loaded yet... + // trigger image loading so it's loaded the next time + self.loadImage(); + return self._image; + + case .LOADED_ERROR: + // B - image loading failed... + // retry loading so it's loaded next time + self.loadImage(); + fallthrough; + + default: + return self._image; + }; + }; + + internal var synthesizedURLRequest: URLRequest? { + guard let url = self.url else { return nil }; + return URLRequest(url: url); + }; + + internal var fallbackImage: UIImage? { + self.fallbackImageConfig?.image + }; + + // MARK: - Init + // ------------ + + internal init?( + dict: NSDictionary, + imageLoadingConfig: NSDictionary?, + onImageDidLoadBlock: ImageDidLoadHandler? = nil + ){ + guard let urlString = dict["url"] as? String + else { return nil }; + + self.urlString = urlString; + self.url = URL(string: urlString); + + self.fallbackImageConfig = { + guard let rawConfig = dict["fallbackImage"] as? NSDictionary + else { return nil }; + + return RNIImageItem(dict: rawConfig); + }(); + + self.imageLoadingConfig = + RNIRemoteURLImageLoadingConfig(dict: imageLoadingConfig ?? [:]); + + self.onImageDidLoadBlock = onImageDidLoadBlock; + + if self.url != nil { + self.setup(); + + } else if self.fallbackImage != nil { + // B - Failed to construct URL instance from string... + // Use fallback image. + self.state = .LOADED_ERROR(); + + } else { + // C - Failed to construct URL instance from string and no fallback image + // is available + return nil; + }; + }; + + // MARK: Functions + // --------------- + + private func setup(){ + let cachedImage = self.cachedImage; + + let shouldLazyLoad = self.imageLoadingConfig.shouldLazyLoad ?? false; + let shouldUseCache = self.imageLoadingConfig.shouldCache ?? false; + + /// Either: + /// * A - no cache exists for the provided url string + /// * B - image caching has been disabled + let hasCachedImage = shouldUseCache && cachedImage != nil; + + let shouldPreloadImage = !shouldLazyLoad && !hasCachedImage; + + if shouldPreloadImage { + // A - Load image in the bg, so it's potentially ready when the image is + // accessed later... + self.loadImage(); + + } else if hasCachedImage { + // B - Use the cached image that matched with the provided url + self.state = .LOADED(image: cachedImage!); + }; + }; + + internal func loadImage(){ + // still has retry attempts remaining, and not currently loading + let shouldLoad = + self.shouldRetry && !self.state.isFinalState; + + guard shouldLoad, + let urlRequest = self.synthesizedURLRequest, + let imageLoader = self.imageLoader + else { + return; + }; + + let prevError = self.state.error; + let hasPrevError = prevError != nil; + + self.state = hasPrevError + // A - Retry loading the remote image + ? .RETRYING(prevError: prevError!) + // B - Loading the remote image for the 1st time + : .LOADING; + + self.loadingAttemptsCount += 1; + let prevImage = self._image; + + imageLoader.loadImage(with: urlRequest){ [weak self] in + guard let strongSelf = self else { return }; + + if let error = $0 { + strongSelf.state = strongSelf.shouldRetry + // A - Error Loading - Try again + ? .LOADED_ERROR(error: error) + // B - Error Loading - Final attempt + : .LOADED_ERROR_FINAL(error: error); + + let nextImage = strongSelf._image; + + if !RNIUtilities.compareImages(prevImage, nextImage) { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return }; + + // failed to load image, but is currently using fallback image, so + // notify that it's using the fallback image as a substitute + strongSelf.onImageDidLoadBlock?(false, strongSelf); + }; + }; + + if strongSelf.imageLoadingConfig._shouldImmediatelyRetryLoading { + strongSelf.loadImage(); + }; + + } else if let image = $1 { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return }; + + strongSelf.state = .LOADED(image: image); + strongSelf.onImageDidLoadBlock?(true, strongSelf); + + if strongSelf.imageLoadingConfig.shouldCache ?? false { + Self.imageCache[strongSelf.urlString] = image; + }; + }; + }; + }; + }; +}; + +// MARK: - RNIRemoteURLImageLoadingConfig - Defaults +// ------------------------------------------------- + +fileprivate extension RNIRemoteURLImageLoadingConfig { + var _shouldLazyLoad: Bool { + self.shouldLazyLoad ?? false; + }; + + var _shouldCache: Bool { + self.shouldCache ?? false; + }; + + var _maxRetryAttempts: Int { + self.maxRetryAttempts ?? 3; + }; + + var _shouldImmediatelyRetryLoading: Bool { + self.shouldImmediatelyRetryLoading ?? false; + }; + + var _fallbackBehavior: FallbackBehavior { + self.fallbackBehavior ?? .onLoadError; + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageRequireMaker.swift b/ios/Temp/RNIImage/RNIImageRequireMaker.swift new file mode 100644 index 00000000..368bdfd7 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageRequireMaker.swift @@ -0,0 +1,76 @@ +// +// RNIImageRequireMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 10/4/22. +// + +import UIKit +import React + + +internal class RNIImageRequireMaker { + static private var imageCache: [String: UIImage] = [:]; + + internal let uri: String; + internal let imageLoadingConfig: RNIImageLoadingConfig; + + internal let rawConfig: NSDictionary; + + internal lazy var image: UIImage? = { + let shouldCache = self.imageLoadingConfig._shouldCache; + + if shouldCache, + let cachedImage = Self.imageCache[self.uri] { + + // A - Use cached image + return cachedImage; + }; + + // B - No cached image + let image = RCTConvert.uiImage(self.rawConfig); + + if shouldCache { + Self.imageCache[self.uri] = image; + }; + + return image; + }(); + + internal init?( + dict: NSDictionary, + imageLoadingConfig loadingConfigDict: NSDictionary? + ){ + guard let uriString = dict["uri"] as? String + else { return nil }; + + self.uri = uriString; + self.rawConfig = dict; + + self.imageLoadingConfig = + RNIImageLoadingConfig(dict: loadingConfigDict ?? [:]); + + self.preloadImageIfNeeded(); + }; + + private func preloadImageIfNeeded(){ + guard !self.imageLoadingConfig._shouldLazyLoad + else { return }; + + // trigger loading of image + _ = self.image; + }; +}; + +// MARK: - RNIImageLoadingConfig - Defaults +// ---------------------------------------- + +fileprivate extension RNIImageLoadingConfig { + var _shouldLazyLoad: Bool { + self.shouldLazyLoad ?? false; + }; + + var _shouldCache: Bool { + self.shouldCache ?? false; + }; +}; diff --git a/ios/Temp/RNIImage/RNIImageSystemMaker.swift b/ios/Temp/RNIImage/RNIImageSystemMaker.swift new file mode 100644 index 00000000..71363867 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageSystemMaker.swift @@ -0,0 +1,113 @@ +// +// RNIImageSystemMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import UIKit +import UIKit + + +internal struct RNIImageSystemMaker { + internal let systemName: String; + + internal let pointSize: CGFloat?; + internal let weight: String?; + internal let scale: String?; + + internal let hierarchicalColor: UIColor?; + internal let paletteColors: [UIColor]?; + + @available(iOS 13.0, *) + internal var symbolConfigs: [UIImage.SymbolConfiguration] { + var configs: [UIImage.SymbolConfiguration] = []; + + if let pointSize = self.pointSize { + configs.append( .init(pointSize: pointSize) ); + }; + + if let string = self.weight, + let weight = UIImage.SymbolWeight(string: string) { + + configs.append( .init(weight: weight) ); + }; + + if let string = self.scale, + let scale = UIImage.SymbolScale(string: string) { + + configs.append( .init(scale: scale) ); + }; + + #if swift(>=5.5) + if #available(iOS 15.0, *), + let color = self.hierarchicalColor { + + configs.append( .init(hierarchicalColor: color) ); + }; + + if #available(iOS 15.0, *), + let colors = self.paletteColors { + + configs.append( .init(paletteColors: colors) ); + }; + #endif + + return configs; + }; + + @available(iOS 13.0, *) + internal var symbolConfig: UIImage.SymbolConfiguration? { + var combinedConfig: UIImage.SymbolConfiguration?; + + for config in symbolConfigs { + if let prevCombinedConfig = combinedConfig { + combinedConfig = prevCombinedConfig.applying(config); + + } else { + combinedConfig = config; + }; + }; + + return combinedConfig; + }; + + @available(iOS 13.0, *) + internal var image: UIImage? { + if let symbolConfig = symbolConfig { + return UIImage( + systemName: self.systemName, + withConfiguration: symbolConfig + ); + }; + + return UIImage(systemName: self.systemName); + }; + + internal init?(dict: NSDictionary){ + guard let systemName = dict["systemName"] as? String + else { return nil }; + + self.systemName = systemName; + + self.pointSize = dict["pointSize"] as? CGFloat; + self.weight = dict["weight" ] as? String; + self.scale = dict["scale" ] as? String; + + self.hierarchicalColor = { + guard let value = dict["hierarchicalColor"], + let color = UIColor.parseColor(value: value) + else { return nil }; + + return color; + }(); + + self.paletteColors = { + guard let items = dict["paletteColors"] as? Array + else { return nil }; + + return items.compactMap { UIColor.parseColor(value: $0) }; + }(); + }; +}; + diff --git a/ios/Temp/RNIImage/RNIImageType.swift b/ios/Temp/RNIImage/RNIImageType.swift new file mode 100644 index 00000000..36518165 --- /dev/null +++ b/ios/Temp/RNIImage/RNIImageType.swift @@ -0,0 +1,29 @@ +// +// RNIImageType.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import UIKit + + +internal enum RNIImageType: String { + case IMAGE_ASSET; + case IMAGE_SYSTEM; + case IMAGE_REQUIRE; + case IMAGE_EMPTY; + case IMAGE_RECT; + case IMAGE_GRADIENT; + case IMAGE_REMOTE_URL; +}; + +internal enum RNIImageConfig { + case IMAGE_ASSET(assetName: String); + case IMAGE_SYSTEM(config: RNIImageSystemMaker); + case IMAGE_REQUIRE(config: RNIImageRequireMaker); + case IMAGE_EMPTY; + case IMAGE_RECT(config: RNIImageMaker); + case IMAGE_GRADIENT(config: RNIImageGradientMaker); + case IMAGE_REMOTE_URL(config: RNIImageRemoteURLMaker); +}; diff --git a/ios/Temp/RNIImage/RNIRemoteURLImageLoadingConfig.swift b/ios/Temp/RNIImage/RNIRemoteURLImageLoadingConfig.swift new file mode 100644 index 00000000..9dc8f42e --- /dev/null +++ b/ios/Temp/RNIImage/RNIRemoteURLImageLoadingConfig.swift @@ -0,0 +1,52 @@ +// +// RNIRemoteURLImageLoadingConfig.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 10/2/22. +// + +import UIKit + + + +/// Maps to: `ImageRemoteURLLoadingConfig` +internal struct RNIRemoteURLImageLoadingConfig: RNIImageLoadingConfigurable { + + // MARK: Embedded Types + // -------------------- + + /// Maps to: `ImageRemoteURLFallbackBehavior` + internal enum FallbackBehavior: String { + case afterFinalAttempt; + case whileNotLoaded; + case onLoadError; + }; + + // MARK: Properties + // ---------------- + + internal let shouldCache: Bool?; + internal let shouldLazyLoad: Bool?; + + internal let maxRetryAttempts: Int?; + internal let shouldImmediatelyRetryLoading: Bool?; + internal let fallbackBehavior: FallbackBehavior?; + + // MARK: Init + // ---------- + + internal init(dict: NSDictionary) { + self.shouldCache = dict["shouldCache"] as? Bool; + self.shouldLazyLoad = dict["shouldLazyLoad"] as? Bool; + + self.maxRetryAttempts = dict["maxRetryAttempts"] as? Int; + self.shouldImmediatelyRetryLoading = dict["shouldImmediatelyRetryLoading"] as? Bool; + + self.fallbackBehavior = { + guard let string = dict["fallbackBehavior"] as? String + else { return nil }; + + return FallbackBehavior(rawValue: string); + }(); + }; +}; diff --git a/ios/Temp/RNINavigationEventsReporting/RNINavigationEventsNotifiable.swift b/ios/Temp/RNINavigationEventsReporting/RNINavigationEventsNotifiable.swift new file mode 100644 index 00000000..dcbc931f --- /dev/null +++ b/ios/Temp/RNINavigationEventsReporting/RNINavigationEventsNotifiable.swift @@ -0,0 +1,14 @@ +// +// RNIContextMenu.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 2/1/22. +// + +import UIKit + +internal protocol RNINavigationEventsNotifiable: NSObject { + + func notifyViewControllerDidPop(sender: RNINavigationEventsReportingViewController); + +}; diff --git a/ios/Temp/RNINavigationEventsReporting/RNINavigationEventsReportingViewController.swift b/ios/Temp/RNINavigationEventsReporting/RNINavigationEventsReportingViewController.swift new file mode 100644 index 00000000..30ca9803 --- /dev/null +++ b/ios/Temp/RNINavigationEventsReporting/RNINavigationEventsReportingViewController.swift @@ -0,0 +1,65 @@ +// +// RNINavigationEventsReportingViewController.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 2/1/22. +// + +import UIKit; + + +/// When added as a child VC, it will listen to the parent VC + navigation controller for navigation events +/// and report them to it's delegate and root view. +internal class RNINavigationEventsReportingViewController: UIViewController { + + internal weak var parentVC: UIViewController?; + internal weak var delegate: RNINavigationEventsNotifiable?; + + // MARK: - Init + // ------------ + + internal init() { + super.init(nibName: nil, bundle: nil); + }; + + // loaded from a storyboard + internal required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder); + }; + + // MARK: - Lifecycle + // ----------------- + + internal override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated); + + guard let navVC = self.navigationController, + let parentVC = self.parentVC + else { return }; + + // if parent VC still exist in the stack, + // then it hasn't been popped yet... + let isParentVCPopped = { + !navVC.viewControllers.contains(parentVC) + }; + + guard isParentVCPopped() + else { return }; + + let notifyDelegate = { + self.delegate?.notifyViewControllerDidPop(sender: self); + }; + + if animated, + let transitionCoordinator = parentVC.transitionCoordinator { + + transitionCoordinator.animate(alongsideTransition: nil){ _ in + guard isParentVCPopped() else { return }; + notifyDelegate(); + }; + + } else { + notifyDelegate(); + }; + }; +}; diff --git a/ios/Temp/RNIUtilities/RNIUtilities.swift b/ios/Temp/RNIUtilities/RNIUtilities.swift new file mode 100644 index 00000000..081e617b --- /dev/null +++ b/ios/Temp/RNIUtilities/RNIUtilities.swift @@ -0,0 +1,165 @@ +// +// RNIUtilities.swift +// IosNavigatorExample +// +// Created by Dominic Go on 1/9/21. +// + +import UIKit; +import React; + + +internal class RNIUtilities { + + internal static weak var sharedBridge: RCTBridge?; + + internal static let osVersion = ProcessInfo().operatingSystemVersion; + + /// If you remove a "react view" from the view hierarchy (e.g. via + /// `removeFromSuperview`), it won't be released, because it's being retained + /// by the `_viewRegistry` ivar in the shared `UIManager` (singleton) instance. + /// + /// The `_viewRegistry` keeps a ref. to all of the "react views" in the app. + /// This explains how you can get a ref. to a view via `viewForReactTag` and + /// `viewForNativeID` (which also means that `reactTag`/`node` is just an index, + /// i.e. it's just a number that gets inc. every-time a "react view" is added). + /// + /// If you are **absolutely sure** that a particular `reactView` is no longer + /// being used, this helper func. will remove `reactView` (and all of it's + /// subviews) in the `_viewRegistry`. + internal static func recursivelyRemoveFromViewRegistry(bridge: RCTBridge, reactView: UIView) { + + func getRegistry(forKey key: String) -> NSMutableDictionary? { + return bridge.uiManager?.value(forKey: key) as? NSMutableDictionary; + }; + + /// Get a ref to the `_viewRegistry` ivar in the `RCTUIManager` instance. + /// * Note: Unlike objc properties, ivars are "private" so they aren't + /// automagically exposed/bridged to swift. + /// * Note: key: `NSNumber` (the `reactTag`), and value: `UIView` + guard let viewRegistry = getRegistry(forKey: "_viewRegistry") + else { return }; + + /// The "react tags" of the views that were removed + var removedViewTags: [NSNumber] = []; + + func removeView(_ v: UIView){ + /// if this really is a "react view" then it should have a `reactTag` + if let reactTag = v.reactTag, + viewRegistry[reactTag] != nil { + + removedViewTags.append(reactTag); + + /// remove from view hierarchy + v.removeFromSuperview(); + + /// remove this "react view" from the registry + viewRegistry.removeObject(forKey: reactTag); + }; + + /// remove other subviews... + v.subviews.forEach { + removeView($0); + }; + + /// remove other react subviews... + v.reactSubviews()?.forEach { + removeView($0); + }; + }; + + func removeShadowViews(){ + /// Get a ref to the `_shadowViewRegistry` ivar in the `RCTUIManager` instance. + /// Note: Execute on "RCT thread" (i.e. "com.facebook.react.ShadowQueue") + guard let shadowViewRegistry = getRegistry(forKey: "_shadowViewRegistry") + else { return }; + + for reactTag in removedViewTags { + shadowViewRegistry.removeObject(forKey: reactTag); + }; + }; + + DispatchQueue.main.async { + // start recursively removing views... + removeView(reactView); + + // remove shadow views... + RCTExecuteOnUIManagerQueue { + removeShadowViews(); + }; + }; + }; + + /// Recursive climb the responder chain until `T` is found. + /// Useful for finding the corresponding view controller of a view. + internal static func getParent(responder: UIResponder, type: T.Type) -> T? { + var parentResponder: UIResponder? = responder; + + while parentResponder != nil { + parentResponder = parentResponder?.next; + + if let parent = parentResponder as? T { + return parent; + }; + }; + + return nil; + }; + + internal static func getView( + forNode node: NSNumber, + type: T.Type, + bridge: RCTBridge? + ) -> T? { + guard let bridge = bridge, + let view = bridge.uiManager?.view(forReactTag: node) + else { return nil }; + + return view as? T; + }; + + internal static func recursivelyGetAllSubviews(for view: UIView) -> [UIView] { + var views: [UIView] = []; + + for subview in view.subviews { + views += Self.recursivelyGetAllSubviews(for: subview); + views.append(subview); + }; + + return views; + }; + + internal static func recursivelyGetAllSuperViews(for view: UIView) -> [UIView] { + var views: [UIView] = []; + + if let parentView = view.superview { + views.append(parentView); + views += Self.recursivelyGetAllSuperViews(for: parentView); + }; + + return views; + }; + + internal static func compareImages(_ a: UIImage?, _ b: UIImage?) -> Bool { + if (a == nil && b == nil){ + // both are nil, equal + return true; + + } else if a == nil || b == nil { + // one is nil, not equal + return false; + + } else if a! === b! { + // same ref to the object, true + return true; + + } else if a!.size == b!.size { + // size diff, not equal + return true; + }; + + // compare raw data + return a!.isEqual(b!); + }; +}; + diff --git a/ios/Temp/RNIUtilities/RNIUtilitiesModule.m b/ios/Temp/RNIUtilities/RNIUtilitiesModule.m new file mode 100644 index 00000000..fe3e605e --- /dev/null +++ b/ios/Temp/RNIUtilities/RNIUtilitiesModule.m @@ -0,0 +1,14 @@ +// +// RNIUtilitiesModule.m +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +#import "React/RCTBridgeModule.h" + +@interface RCT_EXTERN_MODULE(RNIUtilitiesModule, NSObject) + +RCT_EXTERN_METHOD(initialize:(NSDictionary *)params); + +@end diff --git a/ios/Temp/RNIUtilities/RNIUtilitiesModule.swift b/ios/Temp/RNIUtilities/RNIUtilitiesModule.swift new file mode 100644 index 00000000..5982cc54 --- /dev/null +++ b/ios/Temp/RNIUtilities/RNIUtilitiesModule.swift @@ -0,0 +1,29 @@ +// +// RNIUtilitiesModule.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +import UIKit +import React + + +@objc(RNIUtilitiesModule) +internal class RNIUtilitiesModule: NSObject { + + @objc internal var bridge: RCTBridge! { + willSet { + RNIUtilities.sharedBridge = newValue; + } + }; + + @objc internal static func requiresMainQueueSetup() -> Bool { + // run init in bg thread + return false; + }; + + @objc internal func initialize(_ params: NSDictionary){ + // no-op + }; +}; diff --git a/ios/Temp/RNIWrapperView/RNIWrapperView.swift b/ios/Temp/RNIWrapperView/RNIWrapperView.swift new file mode 100644 index 00000000..d2a19a7b --- /dev/null +++ b/ios/Temp/RNIWrapperView/RNIWrapperView.swift @@ -0,0 +1,273 @@ +// +// RNIWrapperView.swift +// IosNavigatorExample +// +// Created by Dominic Go on 2/1/21. +// + +import UIKit +import React + +/// Holds react views that have been detached, and are no longer managed by RN. +internal class RNIWrapperView: UIView { + + internal static var detachedViews = NSMapTable.init( + keyOptions: .copyIn, + valueOptions: .weakMemory + ); + + // MARK: - Properties + // ------------------ + + internal private(set) var bridge: RCTBridge!; + + internal weak var delegate: RNIWrapperViewEventsNotifiable?; + + /// When `shouldAutoDetachSubviews` is enabled, all the child views that were removed from + /// its parent will be stored here. + /// + /// This is only usually used when `isDummyView` is enabled. + internal var reactViews: [UIView] = []; + + internal var touchHandlers: Dictionary = [:]; + + // MARK: - Properties - Flags + // -------------------------- + + /// Whether or not `cleanup` was triggered. + internal private(set) var didTriggerCleanup = false; + + /// Set this property to `true` before moving this view somewhere else (i.e. + /// before calling `removeFromSuperView`). + /// + /// Setting this property to `true` will prevent triggering `cleanup` when removing this view + /// from it's parent view... + /// + /// After you've finished moving this view, set this back to `false`. + internal var isMovingToParent = false; + + internal var shouldDelayAutoCleanupOnJSUnmount = true; + + // MARK: - RN Exported Props - Config - Lifecycle Related + // ------------------------------------------------------ + + /// When this prop is set to `true`, the JS component will trigger + /// `shouldNotifyComponentWillUnmount` during `componentWillUnmount`. + @objc internal private(set) var shouldNotifyComponentWillUnmount: Bool = false; + + /// This property determines whether `cleanup` should be called when + /// `shouldNotifyComponentWillUnmount` is called. Defaults to: `true`. + /// + /// When `shouldNotifyComponentWillUnmount` prop is true, the JS component + /// will notify it's corresponding native view that it'll be unmounted (i.e. + /// via calling the `shouldNotifyComponentWillUnmount` method). + /// + /// * When a react view is "detached" (i.e. when `removeFromSuperView` is called), + /// the react view is on it's own (the view will leak since it won't be removed + /// when the corresponding view comp. un-mounts). + /// + /// * To get around this, the js comp. notifies the native view that it's + /// going to be unmount in `componentWillUnmount` react lifecycle. + /// + /// * This also fixes the issue where the js comp. has already been unmounted, + /// but it's corresponding native view is still being used. + /// + @objc internal private(set) var shouldAutoCleanupOnJSUnmount = false; + + /// Determines whether `cleanup` is called when this view is removed from the + /// view hierarchy (i.e. when the window ref. becomes nil). + @objc internal private(set) var shouldAutoCleanupOnWindowNil = false; + + /// Determines whether `layoutSubviews` will automatically trigger + /// `notifyForBoundsChange`. Defaults to `true`. + /// + /// * If the layout size is determined from the react/js side, set this to `false`. + /// + /// * Otherwise if the layout size is determined from the native side (e.g. via + /// the view controller, etc.) then set this to `true`. + @objc internal private(set) var shouldAutoSetSizeOnLayout = false; + + // MARK: - RN Exported Props - Config - "Dummy View"-Related + // --------------------------------------------------------- + + /// When set to `true`, the view itself is not the one that's being used for content, as such its a + /// "dummy" view (i.e. its not going to be displayed or used). + /// + /// In this mode, it's child views are the ones that are being used for content, and the parent view will + /// usually get removed from the view hierarchy. + @objc internal private(set) var isDummyView = false; + + /// When enabled, the child views will be automatically removed from it's parent, and will be stored in + /// the `reactViews` property. + /// + /// This is usually enabled together with `isDummyView`. + @objc internal private(set) var shouldAutoDetachSubviews = false; + + // MARK: - RN Exported Props - Config - Touch Handlers + // --------------------------------------------------- + + /// If you are planning on removing the parent view (i.e. this view instance) from the view hierarchy via + /// calling `removeFromSuperview`, and you still want it to receive touch events , then set this + /// property to `true`. + /// + /// When in dummy mode, `shouldAutoDetachSubviews` is usually also enabled. + @objc internal private(set) var shouldCreateTouchHandlerForParentView = false; + + /// If you are planning on removing the subviews from the view hierarchy (i.e. using "dummy view" mode), + /// and you still want them to receive touch event, then set this property to `true`. + @objc internal private(set) var shouldCreateTouchHandlerForSubviews = false; + + + // MARK: - Init/Lifecycle + // --------------------- + + init(bridge: RCTBridge) { + super.init(frame: CGRect()); + self.bridge = bridge; + + if self.shouldCreateTouchHandlerForParentView { + self.touchHandlers[self.reactTag] = { + let handler = RCTTouchHandler(bridge: self.bridge)!; + handler.attach(to: self); + + return handler; + }(); + }; + }; + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented"); + }; + + internal override func layoutSubviews() { + super.layoutSubviews(); + + if self.shouldAutoSetSizeOnLayout { + self.notifyForBoundsChange(size: self.bounds.size); + }; + }; + + internal override func didMoveToWindow() { + + let isMovingToWindowNil = self.window == nil; + + let isOrphaned = self.isDummyView + ? self.reactViews.allSatisfy { $0.superview == nil } + : self.superview == nil; + + /// A: Prevent `cleanup` when changing parent views + /// B: Only trigger cleanup when moving to a `nil` window + let shouldTriggerCleanup = + !self.isMovingToParent && isMovingToWindowNil && isOrphaned; + + if shouldTriggerCleanup { + self.cleanup(); + }; + }; + + internal override func removeFromSuperview() { + super.removeFromSuperview(); + Self.detachedViews.setObject(self, forKey: self.reactTag); + } + + // MARK: - React Lifecycle + // ---------------------- + + internal override func insertReactSubview(_ subview: UIView!, at atIndex: Int) { + super.insertSubview(subview, at: atIndex); + + if self.shouldAutoDetachSubviews { + self.reactViews.append(subview); + subview.removeFromSuperview(); + }; + + if self.shouldCreateTouchHandlerForSubviews { + self.touchHandlers[subview.reactTag] = { + let handler = RCTTouchHandler(bridge: self.bridge); + handler?.attach(to: subview); + + return handler; + }(); + }; + }; + + // MARK: - Functions + // ------------------ + + internal func notifyForBoundsChange(size: CGSize){ + if self.isDummyView { + self.notifyForBoundsChangeForContent(size: size); + + } else { + self.notifyForBoundsChangeForWrapper(size: size); + }; + }; + + internal func notifyForBoundsChangeForWrapper(size: CGSize){ + guard let bridge = self.bridge else { return }; + bridge.uiManager.setSize(size, for: self); + }; + + internal func notifyForBoundsChangeForContent(size: CGSize){ + guard let bridge = self.bridge + else { return }; + + let views = self.isDummyView + ? self.reactViews + : self.subviews; + + for view in views { + bridge.uiManager.setSize(size, for: view); + }; + }; + + // MARK: - Commands For Module + // -------------------------- + + /// Called by `RNIWrapperViewModule.notifyComponentWillUnmount` + internal func onJSComponentWillUnmount(isManuallyTriggered: Bool){ + self.delegate?.onJSComponentWillUnmount( + sender: self, + isManuallyTriggered: isManuallyTriggered + ); + + guard self.shouldAutoCleanupOnJSUnmount else { return }; + + if self.shouldDelayAutoCleanupOnJSUnmount { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + self?.cleanup(); + }; + + } else { + self.cleanup(); + }; + }; +}; + + +// MARK: - RNICleanable +// -------------------- + +extension RNIWrapperView: RNICleanable { + + internal func cleanup(){ + guard !self.didTriggerCleanup else { return }; + self.didTriggerCleanup = true; + + let viewsToCleanup = self.reactViews + [self]; + + for view in viewsToCleanup { + if let touchHandler = self.touchHandlers[view.reactTag] { + touchHandler.detach(from: view); + }; + + RNIUtilities.recursivelyRemoveFromViewRegistry( + bridge: self.bridge, + reactView: view + ); + }; + + self.touchHandlers.removeAll(); + self.reactViews.removeAll(); + }; +}; diff --git a/ios/Temp/RNIWrapperView/RNIWrapperViewEventsNotifiable.swift b/ios/Temp/RNIWrapperView/RNIWrapperViewEventsNotifiable.swift new file mode 100644 index 00000000..de401f52 --- /dev/null +++ b/ios/Temp/RNIWrapperView/RNIWrapperViewEventsNotifiable.swift @@ -0,0 +1,16 @@ +// +// RNIWrapperViewEventsNotifiable.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 7/17/22. +// + +import Foundation + + +internal protocol RNIWrapperViewEventsNotifiable: AnyObject { + func onJSComponentWillUnmount( + sender: RNIWrapperView, + isManuallyTriggered: Bool + ); +}; diff --git a/ios/Temp/RNIWrapperView/RNIWrapperViewManager.m b/ios/Temp/RNIWrapperView/RNIWrapperViewManager.m new file mode 100644 index 00000000..2e434632 --- /dev/null +++ b/ios/Temp/RNIWrapperView/RNIWrapperViewManager.m @@ -0,0 +1,28 @@ +// +// RNIWrapperViewManager.m +// IosNavigatorExample +// +// Created by Dominic Go on 2/1/21. +// + +#import + +@interface RCT_EXTERN_MODULE(RNIWrapperViewManager, RCTViewManager) + +// MARK: - Export Props - Values +// ----------------------------- + +RCT_EXPORT_VIEW_PROPERTY(shouldNotifyComponentWillUnmount, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoCleanupOnJSUnmount, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoCleanupOnWindowNil, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoSetSizeOnLayout, BOOL); + +RCT_EXPORT_VIEW_PROPERTY(isDummyView, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoDetachSubviews, BOOL); + +RCT_EXPORT_VIEW_PROPERTY(shouldCreateTouchHandlerForParentView, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldCreateTouchHandlerForSubviews, BOOL); + + + +@end diff --git a/ios/Temp/RNIWrapperView/RNIWrapperViewManager.swift b/ios/Temp/RNIWrapperView/RNIWrapperViewManager.swift new file mode 100644 index 00000000..a5f11ea1 --- /dev/null +++ b/ios/Temp/RNIWrapperView/RNIWrapperViewManager.swift @@ -0,0 +1,38 @@ +// +// RNIWrapperViewManager.swift +// IosNavigatorExample +// +// Created by Dominic Go on 2/1/21. +// + +import Foundation +import React + +@objc(RNIWrapperViewManager) +internal class RNIWrapperViewManager: RCTViewManager { + + static var sharedBridge: RCTBridge?; + + // MARK: - RN Module Setup + // ----------------------- + + override static func requiresMainQueueSetup() -> Bool { + // run init in bg thread + return false; + }; + + override func view() -> UIView! { + // save a ref to this module's RN bridge instance + if Self.sharedBridge == nil { + Self.sharedBridge = self.bridge; + }; + + return RNIWrapperView(bridge: self.bridge); + }; + + func invalidate(){ + /// reset ref to RCTBridge instance + Self.sharedBridge = nil; + }; +}; + diff --git a/ios/Temp/RNIWrapperView/RNIWrapperViewModule.m b/ios/Temp/RNIWrapperView/RNIWrapperViewModule.m new file mode 100644 index 00000000..3268f775 --- /dev/null +++ b/ios/Temp/RNIWrapperView/RNIWrapperViewModule.m @@ -0,0 +1,15 @@ +// +// RNIWrapperViewModule.m +// react-native-ios-navigator +// +// Created by Dominic Go on 8/3/21. +// + +#import "React/RCTBridgeModule.h" + +@interface RCT_EXTERN_MODULE(RNIWrapperViewModule, NSObject) + +RCT_EXTERN_METHOD(notifyComponentWillUnmount:(nonnull NSNumber *)node + params:(NSDictionary *)params); + +@end diff --git a/ios/Temp/RNIWrapperView/RNIWrapperViewModule.swift b/ios/Temp/RNIWrapperView/RNIWrapperViewModule.swift new file mode 100644 index 00000000..b0816f26 --- /dev/null +++ b/ios/Temp/RNIWrapperView/RNIWrapperViewModule.swift @@ -0,0 +1,55 @@ +// +// RNIWrapperViewModule.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 8/3/21. +// + +import Foundation +import React + +@objc(RNIWrapperViewModule) +internal class RNIWrapperViewModule: NSObject { + + @objc internal private(set) var bridge: RCTBridge!; + + @objc static func requiresMainQueueSetup() -> Bool { + // run init in bg thread + return false; + }; + + func getWrapperView(_ node: NSNumber) -> RNIWrapperView? { + return RNIUtilities.getView( + forNode: node, + type : RNIWrapperView.self, + bridge : self.bridge + ); + }; + + // MARK: - Module Commands: Navigator + // --------------------------------- + + @objc func notifyComponentWillUnmount( + _ node: NSNumber, + params: NSDictionary + ){ + DispatchQueue.main.async { + // get `RNIWrapperView` instance that matches node/reactTag + guard let wrapperView = self.getWrapperView(node) else { + #if DEBUG + print( + "LOG - ViewManager, RNIWrapperViewModule: notifyComponentWillUnmount" + + " - for node: \(node)" + + " - no corresponding view found for node" + + " - the view might have already been unmounted..." + ); + #endif + return; + }; + + wrapperView.onJSComponentWillUnmount( + isManuallyTriggered: params["isManuallyTriggered"] as? Bool ?? false + ); + }; + }; +};