diff --git a/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift b/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift index fbc5c7a7..d6e85c8d 100644 --- a/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift +++ b/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift @@ -137,6 +137,10 @@ extension RNIModalSheetViewDelegate: RNIContentViewDelegate { // MARK: Paper + Fabric // -------------------- + + public func notifyOnInit(sender: RNIContentViewParentDelegate) { + ModalEventsManagerRegistry.shared.swizzleIfNeeded(); + } public func notifyOnMountChildComponentView( sender: RNIContentViewParentDelegate, diff --git a/ios/Temp/ModalEventManager/ModalEventsManager.swift b/ios/Temp/ModalEventManager/ModalEventsManager.swift new file mode 100644 index 00000000..2d218d97 --- /dev/null +++ b/ios/Temp/ModalEventManager/ModalEventsManager.swift @@ -0,0 +1,149 @@ +// +// ModalEventsManager.swift +// react-native-ios-modal +// +// Created by Dominic Go on 10/2/24. +// + +import UIKit +import DGSwiftUtilities + + +public final class ModalEventsManager { + + // MARK: - Properties + // ------------------ + + public weak var window: UIWindow?; + private(set) public var modalRegistry: ModalRegistry = .init(); + + // MARK: - Init + // ------------ + + public init(withWindow window: UIWindow){ + self.window = window; + }; + + func registerModalIfNeeded( + _ modalVC: UIViewController, + isAboutToBePresented: Bool + ){ + let window = + self.window + ?? modalVC.view.window + ?? UIApplication.shared.activeWindow; + + guard let window = window else { + return; + }; + + if self.window == nil { + self.window = window; + }; + + let currentModalLevel = window.currentModalLevel ?? -1; + + let nextModalFocusIndex = isAboutToBePresented + ? currentModalLevel + 1 + : modalVC.modalLevel ?? -1; + + self.modalRegistry.registerModalIfNeeded( + modalVC, + modalFocusIndex: nextModalFocusIndex + ); + }; + + // MARK: - Methods Invoked Via Swizzling + // ------------------------------------- + + public func notifyOnModalWillPresent( + forViewController modalVC: UIViewController, + targetWindow: UIWindow? + ){ + + guard let targetWindow = targetWindow else { + return + }; + + let eventManager = + ModalEventsManagerRegistry.shared.getManager(forWindow: targetWindow); + + eventManager.registerModalIfNeeded( + modalVC, + isAboutToBePresented: true + ); + + let modalEntries = eventManager.modalRegistry.getEntriesGrouped(); + + modalEntries.topMostModal!.setModalFocusState(.focusing); + modalEntries.secondTopMostModal?.setModalFocusState(.blurring); + + modalEntries.otherModals?.forEach { + $0.setModalFocusState(.blurred); + }; + }; + + public func notifyOnModalDidPresent( + forViewController modalVC: UIViewController, + targetWindow: UIWindow? + ){ + guard let targetWindow = targetWindow else { + return + }; + + let eventManager = + ModalEventsManagerRegistry.shared.getManager(forWindow: targetWindow); + + let modalEntries = eventManager.modalRegistry.getEntriesGrouped(); + + modalEntries.topMostModal!.setModalFocusState(.focused); + modalEntries.secondTopMostModal?.setModalFocusState(.blurred); + + modalEntries.otherModals?.forEach { + $0.setModalFocusState(.blurred); + }; + }; + + public func notifyOnModalWillDismiss( + forViewController modalVC: UIViewController, + targetWindow: UIWindow? + ){ + guard let targetWindow = targetWindow else { + return + }; + + let eventManager = + ModalEventsManagerRegistry.shared.getManager(forWindow: targetWindow); + + let modalEntries = eventManager.modalRegistry.getEntriesGrouped(); + + modalEntries.topMostModal!.setModalFocusState(.blurring); + modalEntries.secondTopMostModal?.setModalFocusState(.focusing); + + modalEntries.otherModals?.forEach { + $0.setModalFocusState(.blurred); + }; + }; + + public func notifyOnModalDidDismiss( + forViewController modalVC: UIViewController, + targetWindow: UIWindow? + ){ + guard let targetWindow = targetWindow else { + return + }; + + let eventManager = + ModalEventsManagerRegistry.shared.getManager(forWindow: targetWindow); + + let modalEntries = eventManager.modalRegistry.getEntriesGrouped(); + + modalEntries.topMostModal!.setModalFocusState(.blurred); + modalEntries.secondTopMostModal?.setModalFocusState(.focused); + + modalEntries.otherModals?.forEach { + $0.setModalFocusState(.blurred); + }; + }; +}; + diff --git a/ios/Temp/ModalEventManager/ModalEventsManagerRegistry.swift b/ios/Temp/ModalEventManager/ModalEventsManagerRegistry.swift new file mode 100644 index 00000000..c94243d7 --- /dev/null +++ b/ios/Temp/ModalEventManager/ModalEventsManagerRegistry.swift @@ -0,0 +1,148 @@ +// +// ModalEventsManagerRegistry.swift +// react-native-ios-modal +// +// Created by Dominic Go on 10/2/24. +// + +import UIKit +import DGSwiftUtilities + + +public final class ModalEventsManagerRegistry: Singleton { + + public typealias `Self` = ModalEventsManagerRegistry; + + // MARK: Static Properties + // ----------------------- + + public static let shared: Self = .init(); + + // MARK: - Static Properties (Swizzling-Related) + // --------------------------------------------- + + private(set) public static var isSwizzled = false; + public static var shouldSwizzle = true; + + // MARK: - Properties + // ------------------ + + public var instanceRegistry: Dictionary = [:] + + // MARK: - Init + // ------------ + + public init(){ + self.swizzleIfNeeded(); + }; + + // MARK: - Methods + // --------------- + + public func getManager(forWindow window: UIWindow) -> ModalEventsManager { + let match = self.instanceRegistry[window.synthesizedStringID]; + + + if let match = match, + match.window != nil + { + return match; + }; + + let newManager: ModalEventsManager = .init(withWindow: window); + self.instanceRegistry[window.synthesizedStringID] = newManager; + + return newManager; + }; + + // MARK: - Methods (Swizzling-Related) + // ---------------------------------- + + public func swizzleIfNeeded(){ + guard Self.shouldSwizzle, + !Self.isSwizzled + else { return }; + + Self.isSwizzled = true; + self._swizzlePresent(); + self._swizzleDismiss(); + }; + + private func _swizzlePresent(){ + SwizzlingHelpers.swizzlePresent() { originalImp, selector in + return { _self, vcToPresent, animated, completion in + + let currentWindow = + _self.view.window + ?? vcToPresent.view.window + ?? UIApplication.shared.activeWindow; + + guard let currentWindow = currentWindow else { + #if DEBUG + fatalError("Unable to get window") + #else + return; + #endif + }; + + let eventManager = self.getManager(forWindow: currentWindow); + + eventManager.notifyOnModalWillPresent( + forViewController: vcToPresent, + targetWindow: currentWindow + ); + + // Call the original implementation. + originalImp(_self, selector, vcToPresent, animated){ + eventManager.notifyOnModalDidPresent( + forViewController: vcToPresent, + targetWindow: currentWindow + ); + completion?(); + }; + }; + }; + }; + + private func _swizzleDismiss(){ + SwizzlingHelpers.swizzleDismiss() { originalImp, selector in + return { _self, animated, completion in + + let currentWindow = + _self.view.window + ?? _self.presentedViewController?.view.window + ?? UIApplication.shared.activeWindow; + + let modalVC = + _self.presentedViewController + ?? currentWindow?.topmostPresentedViewController; + + guard let currentWindow = currentWindow, + let modalVC = modalVC + else { + #if DEBUG + fatalError("Unable to get window or presented view controller") + #else + return; + #endif + }; + + let eventManager = self.getManager(forWindow: currentWindow); + + eventManager.notifyOnModalWillDismiss( + forViewController: modalVC, + targetWindow: currentWindow + ); + + // Call the original implementation. + originalImp(_self, selector, animated){ + eventManager.notifyOnModalDidDismiss( + forViewController: modalVC, + targetWindow: currentWindow + ); + completion?(); + }; + }; + }; + }; +}; diff --git a/ios/Temp/ModalEventManager/ModalFocusEventNotifiable.swift b/ios/Temp/ModalEventManager/ModalFocusEventNotifiable.swift new file mode 100644 index 00000000..1cdbe78d --- /dev/null +++ b/ios/Temp/ModalEventManager/ModalFocusEventNotifiable.swift @@ -0,0 +1,31 @@ +// +// ModalFocusEventNotifiable.swift +// +// +// Created by Dominic Go on 6/15/24. +// + +import Foundation + +public protocol ModalFocusEventNotifiable: AnyObject { + + func notifyForModalFocusStateChange( + prevState: ModalFocusState?, + currentState: ModalFocusState, + nextState: ModalFocusState + ); +}; + +// MARK: - ModalFocusEventNotifiable+Default +// ----------------------------------------- + +public extension ModalFocusEventNotifiable { + + func notifyForModalFocusStateChange( + prevState: ModalFocusState?, + currentState: ModalFocusState, + nextState: ModalFocusState + ) { + // no-op + }; +}; diff --git a/ios/Temp/ModalEventManager/ModalRegistry.swift b/ios/Temp/ModalEventManager/ModalRegistry.swift new file mode 100644 index 00000000..27aec532 --- /dev/null +++ b/ios/Temp/ModalEventManager/ModalRegistry.swift @@ -0,0 +1,91 @@ +// +// ModalRegistry.swift +// react-native-ios-modal +// +// Created by Dominic Go on 10/2/24. +// + +import UIKit + + +public class ModalRegistry { + + public typealias ModalRegistry = Dictionary; + + public var registry: ModalRegistry = [:]; + + public func registerModalIfNeeded( + _ modalVC: UIViewController, + modalFocusIndex: Int + ){ + let match = self.registry[modalVC.synthesizedStringID]; + + let isRegistered = match != nil; + let shouldOverwrite = match?.isValidEntry ?? false; + + let shouldRegister = !isRegistered || shouldOverwrite; + guard shouldRegister else { + return; + }; + + self.registry[modalVC.synthesizedStringID] = .init( + viewController: modalVC, + modalFocusIndex: modalFocusIndex + ); + }; + + public func getEntry(for modalVC: UIViewController) -> ModalRegistryEntry? { + self.registry[modalVC.synthesizedStringID]; + }; + + public func removeEntry(forViewController modalVC: UIViewController){ + self.registry.removeValue(forKey: modalVC.synthesizedStringID); + }; + + public func removeEntry(forEntry entry: ModalRegistryEntry){ + self.registry.removeValue(forKey: entry.modalInstanceID); + }; + + @discardableResult + public func setModalFocusState( + for modalVC: UIViewController, + withModalFocusState modalFocusStateNext: ModalFocusState + ) -> ModalRegistryEntry? { + guard let match = self.getEntry(for: modalVC) else { + return nil; + }; + + match.setModalFocusState(modalFocusStateNext); + return match; + }; + + public func getRegistry() -> ModalRegistry { + let filtered = self.registry.filter { + $1.isValidEntry + }; + + self.registry = filtered; + return filtered; + }; + + public func getEntriesSorted() -> [ModalRegistryEntry] { + self.registry.values.sorted { + $0.modalFocusIndex > $1.modalFocusIndex; + }; + }; + + public func getEntriesGrouped() -> ( + topMostModal: ModalRegistryEntry?, + secondTopMostModal: ModalRegistryEntry?, + otherModals: [ModalRegistryEntry]? + ) { + var entriesSorted = self.getEntriesSorted(); + + return ( + topMostModal: entriesSorted.popLast(), + secondTopMostModal: entriesSorted.popLast(), + otherModals: entriesSorted + ); + }; +}; + diff --git a/ios/Temp/ModalEventManager/ModalRegistryEntry.swift b/ios/Temp/ModalEventManager/ModalRegistryEntry.swift new file mode 100644 index 00000000..80e30abe --- /dev/null +++ b/ios/Temp/ModalEventManager/ModalRegistryEntry.swift @@ -0,0 +1,56 @@ +// +// ModalRegistryEntry.swift +// react-native-ios-modal +// +// Created by Dominic Go on 10/2/24. +// + +import UIKit + + +public class ModalRegistryEntry { + + public weak var viewController: UIViewController?; + + public var modalFocusIndex: Int; + public var modalInstanceID: String; + + public var modalFocusState: ModalFocusState; + public var modalFocusStatePrev: ModalFocusState?; + + public var isValidEntry: Bool { + self.viewController != nil; + }; + + public init( + viewController: UIViewController, + modalFocusIndex: Int, + modalState: ModalFocusState = .blurred + ) { + self.viewController = viewController; + self.modalFocusIndex = modalFocusIndex; + self.modalFocusState = modalState; + + self.modalInstanceID = viewController.synthesizedStringID; + }; + + public func setModalFocusState(_ modalStateNext: ModalFocusState){ + let modalStatePrev = self.modalFocusStatePrev; + let modalStateCurrent = self.modalFocusState; + + guard modalStateCurrent != modalStateNext else { + return; + }; + + self.modalFocusState = modalStateNext; + self.modalFocusStatePrev = modalStateCurrent; + + if let eventDelegate = self.viewController as? ModalFocusEventNotifiable { + eventDelegate.notifyForModalFocusStateChange( + prevState: modalStatePrev, + currentState: modalStateCurrent, + nextState: modalStateNext + ); + }; + }; +}; diff --git a/ios/Temp/ModalEventManager/UIViewController+ModaRegistryHelpers.swift b/ios/Temp/ModalEventManager/UIViewController+ModaRegistryHelpers.swift new file mode 100644 index 00000000..23b80789 --- /dev/null +++ b/ios/Temp/ModalEventManager/UIViewController+ModaRegistryHelpers.swift @@ -0,0 +1,44 @@ +// +// UIViewController+ModaRegistryHelpers.swift +// react-native-ios-modal +// +// Created by Dominic Go on 10/2/24. +// + +import UIKit + +public extension UIViewController { + + var associatedModalEventManager: ModalEventsManager? { + let window = self.view.window ?? UIApplication.shared.activeWindow; + + guard let window = window else { + return nil; + }; + + return ModalEventsManagerRegistry.shared.getManager(forWindow: window); + }; + + var modalRegistryEntry: ModalRegistryEntry? { + guard let modalEventManager = self.associatedModalEventManager, + let entryMatch = modalEventManager.modalRegistry.getEntry(for: self) + else { + return nil; + }; + + return entryMatch; + }; + + var isRegisteredInModalRegistry: Bool { + self.modalRegistryEntry != nil; + }; + + var modalFocusStatePrev: ModalFocusState? { + self.modalRegistryEntry?.modalFocusStatePrev; + }; + + var modalFocusState: ModalFocusState? { + self.modalRegistryEntry?.modalFocusState; + }; +}; + diff --git a/ios/Temp/ModalFocusState.swift b/ios/Temp/ModalFocusState.swift new file mode 100644 index 00000000..fcfe8fdf --- /dev/null +++ b/ios/Temp/ModalFocusState.swift @@ -0,0 +1,24 @@ +// +// ModalFocusState.swift +// react-native-ios-modal +// +// Created by Dominic Go on 10/2/24. +// + +import Foundation + + +public enum ModalFocusState: String { + case blurred; + case blurring; + case focused; + case focusing; + + public var isFocused: Bool { + self == .focusing || self == .focused; + }; + + public var isBlurred: Bool { + self == .blurring || self == .blurred; + }; +}; diff --git a/ios/Temp/ModalViewControllerLifecycleNotifier.swift b/ios/Temp/ModalViewControllerLifecycleNotifier.swift index 9b8be75b..ba6f17ef 100644 --- a/ios/Temp/ModalViewControllerLifecycleNotifier.swift +++ b/ios/Temp/ModalViewControllerLifecycleNotifier.swift @@ -17,9 +17,17 @@ open class ModalViewControllerLifecycleNotifier: ViewControllerLifecycleNotifier private(set) public var modalLifecycleEventDelegates: MulticastDelegate = .init(); + private(set) public var modalFocusEventDelegates: + MulticastDelegate = .init(); + // MARK: - View Controller Lifecycle // --------------------------------- + public override func viewDidLoad() { + super.viewDidLoad(); + ModalEventsManagerRegistry.shared.swizzleIfNeeded(); + } + public override func viewWillAppear(_ animated: Bool) { defer { super.viewWillAppear(animated); @@ -128,11 +136,51 @@ open class ModalViewControllerLifecycleNotifier: ViewControllerLifecycleNotifier // MARK: - Methods // --------------- + open override func present( + _ viewControllerToPresent: UIViewController, + animated flag: Bool, + completion: (() -> Void)? = nil + ) { + if let modalVC = viewControllerToPresent as? ModalViewControllerLifecycleNotifier { + modalVC.isExplicitlySomeKindOfModal = true; + }; + + super.present( + viewControllerToPresent, + animated: flag, + completion: completion + ); + }; + open override func dismiss( animated flag: Bool, completion: (() -> Void)? = nil ) { self.isExplicitlyBeingDismissed = true; super.dismiss(animated: flag, completion: completion); - } + }; +}; + +extension ModalViewControllerLifecycleNotifier: ModalFocusEventNotifiable { + + public func notifyForModalFocusStateChange( + prevState: ModalFocusState?, + currentState: ModalFocusState, + nextState: ModalFocusState + ) { + print( + "ModalViewControllerLifecycleNotifier.notifyForModalFocusStateChange", + "\n - instanceID", self.synthesizedStringID, + "\n - \(prevState?.rawValue ?? "N/A") -> \(currentState) -> \(nextState)", + "\n" + ); + + self.modalFocusEventDelegates.invoke { + $0.notifyForModalFocusStateChange( + prevState: prevState, + currentState: currentState, + nextState: nextState + ); + }; + }; }; diff --git a/ios/Temp/SwizzlingHelpers+UIViewController.swift b/ios/Temp/SwizzlingHelpers+UIViewController.swift new file mode 100644 index 00000000..f4a1e8cd --- /dev/null +++ b/ios/Temp/SwizzlingHelpers+UIViewController.swift @@ -0,0 +1,84 @@ +// +// SwizzlingHelpers+UIViewController.swift +// +// +// Created by Dominic Go on 6/14/24. +// + +import UIKit +import DGSwiftUtilities + +public extension SwizzlingHelpers { + + @discardableResult + static func swizzlePresent( + /// `UIViewController.present(_:animated:completion)` or: + /// `func present(` + /// ` _ viewControllerToPresent: UIViewController, + /// ` animated: Bool, + /// ` completion: (() -> Void)? + /// `)` + /// + impMethodType: T.Type = (@convention(c) ( + /* self : */ UIViewController, + /* _cmd : */ Selector, + /* vcToPresent: */ UIViewController, + /* animated : */ Bool, + /* completion : */ (() -> Void)? + ) -> Void).self, + + impBlockType: U.Type = (@convention(block) ( + /* self : */ UIViewController, + /* vcToPresent: */ UIViewController, + /* animated : */ Bool, + /* completion : */ (() -> Void)?) -> Void + ).self, + + presentBlockMaker: @escaping ( + _ originalImp: T, + _ selector: Selector + ) -> U + ) -> IMP? { + let selector = #selector(UIViewController.present(_:animated:completion:)); + + return Self.swizzleWithBlock( + impMethodType: impMethodType, + forClass: UIViewController.self, + withSelector: selector, + newImpMaker: presentBlockMaker + ); + }; + + @discardableResult + static func swizzleDismiss( + /// `UIViewController.dismiss(animated:completion)` or: + /// `func dismiss(animated: Bool, completion: (() -> Void)?)` + /// + impMethodType: T.Type = (@convention(c) ( + /* self : */ UIViewController, + /* _cmd : */ Selector, + /* animated : */ Bool, + /* completion : */ (() -> Void)? + ) -> Void).self, + + impBlockType: U.Type = (@convention(block) ( + /* self : */ UIViewController, + /* animated : */ Bool, + /* completion : */ (() -> Void)?) -> Void + ).self, + + dismissBlockMaker: @escaping ( + _ originalImp: T, + _ selector: Selector + ) -> U + ) -> IMP? { + let selector = #selector(UIViewController.dismiss(animated:completion:)); + + return Self.swizzleWithBlock( + impMethodType: impMethodType, + forClass: UIViewController.self, + withSelector: selector, + newImpMaker: dismissBlockMaker + ); + }; +};