diff --git a/ios/RNIModalSheetView/RNIModalSheetDecorationController.swift b/ios/RNIModalSheetView/RNIModalSheetDecorationController.swift new file mode 100644 index 0000000..be6cbdb --- /dev/null +++ b/ios/RNIModalSheetView/RNIModalSheetDecorationController.swift @@ -0,0 +1,179 @@ +// +// RNIModalSheetDecorationController.swift +// react-native-ios-modal +// +// Created by Dominic Go on 10/3/24. +// + +import UIKit +import DGSwiftUtilities +import react_native_ios_utilities + +#if !RCT_NEW_ARCH_ENABLED +import React +#endif + +/// Holds/wraps a `RNIBaseView` instance (i.e. `RNIContentViewParentDelegate`) +/// +open class RNIModalSheetDecorationController: UIViewController { + + public var shouldTriggerDefaultCleanup = true; + + public weak var rootReactView: RNIContentViewParentDelegate?; + + public var positionConfig: AlignmentPositionConfig = .default; + + // MARK: - Computed Properties + // --------------------------- + + public var contentView: RNIContentViewDelegate? { + self.rootReactView?.contentDelegate; + }; + + // MARK: - View Controller Lifecycle + // --------------------------------- + + public override func viewDidLoad() { + guard let rootReactView = self.rootReactView else { + return; + }; + + #if DEBUG && false + self.log(); + #endif + + // MARK: Setup Constraints + #if !RCT_NEW_ARCH_ENABLED + rootReactView.removeAllAncestorConstraints(); + #endif + + rootReactView.translatesAutoresizingMaskIntoConstraints = false; + self.view.addSubview(rootReactView); + + NSLayoutConstraint.activate([ + rootReactView.leadingAnchor.constraint( + equalTo: rootReactView.leadingAnchor + ), + rootReactView.trailingAnchor.constraint( + equalTo: rootReactView.leadingAnchor + ), + rootReactView.bottomAnchor.constraint( + equalTo: rootReactView.bottomAnchor + ), + rootReactView.topAnchor.constraint( + equalTo: rootReactView.topAnchor + ), + ]); + }; + + public override func viewDidLayoutSubviews() { + guard let rootReactView = self.rootReactView else { + return; + }; + + #if DEBUG && false + self.log(); + DispatchQueue.main.asyncAfter(deadline: .now() + 2){ + self.log(); + }; + #endif + }; + + open override func didMove(toParent parent: UIViewController?) { + guard let parent = parent else { + return; + }; + + let constraints = self.positionConfig.createConstraints( + forView: self.view, + attachingTo: parent.view, + enclosingView: parent.view + ); + + self.view.translatesAutoresizingMaskIntoConstraints = false; + NSLayoutConstraint.activate([ + self.view.leadingAnchor.constraint(equalTo: parent.view.leadingAnchor), + self.view.trailingAnchor.constraint(equalTo: parent.view.trailingAnchor), + self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor), + self.view.heightAnchor.constraint(equalToConstant: 100), + ]); + } + + // MARK: Methods + // -------------- + + #if DEBUG + func log(funcString: String = #function){ + print( + "RNIModalSheetDecorationController.\(funcString)", + "\n - positionConfig:", self.positionConfig, + + "\n - window.size:", + self.view.window?.bounds.size.debugDescription ?? "N/A", + + "\n - view.size:", self.view.bounds.size, + + "\n - view.globalFrame:", + self.view.globalFrame?.debugDescription ?? "N/A", + + "\n - view.layer.frame:", + self.view.layer.presentation()?.frame.debugDescription ?? "N/A", + + "\n - superview.size:", + self.view.superview?.bounds.size.debugDescription ?? "N/A", + + "\n - superview.globalFrame:", + self.view.superview?.globalFrame?.debugDescription ?? "N/A", + + "\n - rootReactView.size:", + self.rootReactView?.bounds.size.debugDescription ?? "N/A", + + "\n - rootReactView.cachedLayoutMetrics.contentFrame:", + self.rootReactView?.cachedLayoutMetrics?.contentFrame.debugDescription ?? "N/A", + + "\n - rootReactView.globalFrame:", + self.rootReactView?.globalFrame?.debugDescription ?? "N/A", + + "\n - rootReactView.layer.frame:", + self.rootReactView?.layer.presentation()?.frame.debugDescription ?? "N/A", + + "\n - rootReactView.intrinsicContentSize:", + self.rootReactView?.intrinsicContentSize.debugDescription ?? "N/A", + + "\n - contentDelegate.bounds.size:", + self.rootReactView?.contentDelegate.bounds.size.debugDescription ?? "N/A", + + "\n - contentDelegate.globalFrame:", + self.rootReactView?.contentDelegate.globalFrame?.debugDescription ?? "N/A", + + "\n - contentDelegate.layer.frame:", + self.rootReactView?.contentDelegate.layer.presentation()?.frame.debugDescription ?? "N/A", + + "\n" + ); + }; + #endif +}; + +extension RNIModalSheetDecorationController: RNIViewLifecycle { + + public func notifyOnRequestForCleanup(sender: RNIContentViewParentDelegate) { + guard self.shouldTriggerDefaultCleanup, + self.view.window != nil + else { + return; + }; + + if self.presentingViewController != nil { + self.dismiss(animated: true); + + } else if self.parent != nil { + self.willMove(toParent: nil); + self.view.removeFromSuperview(); + self.removeFromParent(); + + } else { + self.view.removeFromSuperview(); + }; + }; +}; diff --git a/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift b/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift index f3df228..837b36f 100644 --- a/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift +++ b/ios/RNIModalSheetView/RNIModalSheetViewDelegate.swift @@ -15,6 +15,7 @@ public final class RNIModalSheetViewDelegate: UIView, RNIContentView { enum NativeIDKey: String { case mainSheetContent; + case bottomAttachedSheetOverlay; }; public enum Events: String, CaseIterable { @@ -55,6 +56,9 @@ public final class RNIModalSheetViewDelegate: UIView, RNIContentView { public var modalSheetController: RNIModalSheetViewController?; public var sheetMainContentParentView: RNIContentViewParentDelegate?; + public var sheetBottomAttachedOverlayController: RNIModalSheetDecorationController?; + public var sheetBottomAttachedOverlayParentView: RNIContentViewParentDelegate?; + // MARK: - Properties - RNIContentViewDelegate // ------------------------------------------- @@ -117,6 +121,25 @@ public final class RNIModalSheetViewDelegate: UIView, RNIContentView { modalVC.sheetPresentationStateMachine.eventDelegates.add(self); modalVC.modalFocusEventDelegates.add(self); + if let sheetBottomAttachedOverlayParentView = self.sheetBottomAttachedOverlayParentView { + let childVC = RNIModalSheetDecorationController(); + self.sheetBottomAttachedOverlayController = childVC; + + childVC.rootReactView = sheetBottomAttachedOverlayParentView; + + childVC.positionConfig = .init( + horizontalAlignment: .stretchTarget, + verticalAlignment: .targetBottom + ); + + childVC.view.backgroundColor = .red; + childVC.view.alpha = 0.5; + + modalVC.view.addSubview(childVC.view); + modalVC.addChild(childVC); + childVC.didMove(toParent: modalVC); + }; + return modalVC; }; @@ -172,6 +195,9 @@ extension RNIModalSheetViewDelegate: RNIContentViewDelegate { switch nativeIDKey { case .mainSheetContent: self.sheetMainContentParentView = reactView; + + case .bottomAttachedSheetOverlay: + self.sheetBottomAttachedOverlayParentView = reactView; }; }; diff --git a/src/components/ModalSheetContent/ModalSheetBottomAttachedContentOverlay.tsx b/src/components/ModalSheetContent/ModalSheetBottomAttachedContentOverlay.tsx new file mode 100644 index 0000000..1cc2995 --- /dev/null +++ b/src/components/ModalSheetContent/ModalSheetBottomAttachedContentOverlay.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +import { ModalSheetContent } from './ModalSheetContent'; +import { ModalSheetViewNativeIDKeys } from '../ModalSheetView/ModalSheetViewNativeIDKeys'; + +import type { ModalSheetViewBottomAttachedContentOverlayProps } from './ModalSheetBottomAttachedContentOverlayTypes'; + + +export function ModalSheetViewBottomAttachedContentOverlay( + props: React.PropsWithChildren +) { + const { children, ...otherProps } = props; + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/components/ModalSheetContent/ModalSheetBottomAttachedContentOverlayTypes.ts b/src/components/ModalSheetContent/ModalSheetBottomAttachedContentOverlayTypes.ts new file mode 100644 index 0000000..251189b --- /dev/null +++ b/src/components/ModalSheetContent/ModalSheetBottomAttachedContentOverlayTypes.ts @@ -0,0 +1,13 @@ +import type { ModalSheetContentProps } from './ModalSheetContentTypes'; + + +export type ModalSheetViewBottomAttachedContentOverlayInheritedProps = Pick; + +export type ModalSheetViewBottomAttachedContentOverlayBaseProps = { +}; + +export type ModalSheetViewBottomAttachedContentOverlayProps = + & ModalSheetViewBottomAttachedContentOverlayInheritedProps + & ModalSheetViewBottomAttachedContentOverlayBaseProps; \ No newline at end of file diff --git a/src/components/ModalSheetView/ModalSheetViewNativeIDKeys.ts b/src/components/ModalSheetView/ModalSheetViewNativeIDKeys.ts index 4839231..c0dc4ac 100644 --- a/src/components/ModalSheetView/ModalSheetViewNativeIDKeys.ts +++ b/src/components/ModalSheetView/ModalSheetViewNativeIDKeys.ts @@ -2,4 +2,5 @@ export const ModalSheetViewNativeIDKeys = Object.freeze({ mainSheetContent: 'mainSheetContent', + bottomAttachedSheetOverlay: 'bottomAttachedSheetOverlay', }); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6a4f2ab..9f21af0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,9 @@ export * from './components/ModalSheetView/ModalSheetViewTypes'; export * from './components/ModalSheetContent/ModalSheetMainContent'; export * from './components/ModalSheetContent/ModalSheetContentTypes'; +export * from './components/ModalSheetContent/ModalSheetBottomAttachedContentOverlay'; +export * from './components/ModalSheetContent/ModalSheetBottomAttachedContentOverlayTypes'; + export * from './context/ModalSheetViewContext'; export * from './hooks/useModalSheetViewEvents';