diff --git a/Package.swift b/Package.swift index 3034bf515f4335c1aba8c2367cd85d9ef284dccf..b6116630deb1c9f8e99234c1db82286d9f49286e 100644 --- a/Package.swift +++ b/Package.swift @@ -226,6 +226,10 @@ let package = Package( name: "XXModels", package: "client-ios-db" ), + .product( + name: "ScrollViewController", + package: "ScrollViewController" + ), .product( name: "Dependencies", package: "swift-composable-architecture" diff --git a/Sources/AppNavigation/CustomTransitions/BottomTransition.swift b/Sources/AppNavigation/CustomTransitions/BottomTransition.swift index 66b422c1f833a2b6690cea8947ee07f9481ba070..7c40eba0e741610f7cb5864eff1a59c7800fcb4f 100644 --- a/Sources/AppNavigation/CustomTransitions/BottomTransition.swift +++ b/Sources/AppNavigation/CustomTransitions/BottomTransition.swift @@ -1,5 +1,4 @@ import UIKit -import Combine final class BottomTransition: NSObject, UIViewControllerAnimatedTransitioning { enum Direction { @@ -9,11 +8,10 @@ final class BottomTransition: NSObject, UIViewControllerAnimatedTransitioning { let isDismissableOnBackground: Bool var direction: Direction = .present - private let onDismissal: (() -> Void)? + private let onDismissal: () -> Void private weak var darkOverlayView: UIControl? private weak var topConstraint: NSLayoutConstraint? private weak var bottomConstraint: NSLayoutConstraint? - private var cancellables = Set<AnyCancellable>() private var presentedConstraints: [NSLayoutConstraint] = [] private var dismissedConstraints: [NSLayoutConstraint] = [] @@ -21,7 +19,7 @@ final class BottomTransition: NSObject, UIViewControllerAnimatedTransitioning { init( _ isDismissableOnBackground: Bool = true, - onDismissal: (() -> Void)? + onDismissal: @escaping () -> Void ) { self.onDismissal = onDismissal self.isDismissableOnBackground = isDismissableOnBackground @@ -123,7 +121,7 @@ final class BottomTransition: NSObject, UIViewControllerAnimatedTransitioning { }, completion: { [weak self] _ in context.completeTransition(true) - self?.onDismissal?() + self?.onDismissal() } ) } diff --git a/Sources/AppNavigation/CustomTransitions/BottomTransitioningDelegate.swift b/Sources/AppNavigation/CustomTransitions/BottomTransitioningDelegate.swift index b30e549738b953c01702f1ca6897b6f50899fa9c..41e5ef298afc9cc0706548cf2030873b74483c71 100644 --- a/Sources/AppNavigation/CustomTransitions/BottomTransitioningDelegate.swift +++ b/Sources/AppNavigation/CustomTransitions/BottomTransitioningDelegate.swift @@ -23,3 +23,26 @@ final class BottomTransitioningDelegate: NSObject, UIViewControllerTransitioning return transition } } + +final class FullscreenTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { + private var transition: FullscreenTransition? + + func animationController( + forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + transition = FullscreenTransition { [weak self] in + guard let self else { return } + self.transition = nil + } + return transition + } + + func animationController( + forDismissed dismissed: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + transition?.direction = .dismiss + return transition + } +} diff --git a/Sources/AppNavigation/CustomTransitions/FullscreenTransition.swift b/Sources/AppNavigation/CustomTransitions/FullscreenTransition.swift new file mode 100644 index 0000000000000000000000000000000000000000..75d5fd04087571b9660573c2c9d5980f91696c38 --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/FullscreenTransition.swift @@ -0,0 +1,124 @@ +import UIKit + +final class FullscreenTransition: NSObject, UIViewControllerAnimatedTransitioning { + enum Direction { + case present + case dismiss + } + + var direction: Direction = .present + private let onDismissal: () -> Void + private weak var darkOverlayView: UIControl? + private weak var topConstraint: NSLayoutConstraint? + private weak var bottomConstraint: NSLayoutConstraint? + + private var presentedConstraints: [NSLayoutConstraint] = [] + private var dismissedConstraints: [NSLayoutConstraint] = [] + private var presentingController: UIViewController? + + init(onDismissal: @escaping () -> Void) { + self.onDismissal = onDismissal + super.init() + } + + func transitionDuration( + using context: UIViewControllerContextTransitioning? + ) -> TimeInterval { 0.5 } + + func animateTransition( + using context: UIViewControllerContextTransitioning + ) { + switch direction { + case .present: + present(using: context) + case .dismiss: + dismiss(using: context) + } + } + + private func present(using context: UIViewControllerContextTransitioning) { + guard let presentingController = context.viewController(forKey: .from), + let presentedView = context.view(forKey: .to) else { + context.completeTransition(false) + return + } + + let darkOverlayView = UIControl() + self.darkOverlayView = darkOverlayView + + darkOverlayView.alpha = 0.0 + darkOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) + context.containerView.addSubview(darkOverlayView) + darkOverlayView.frame = context.containerView.bounds + + darkOverlayView.addTarget(self, action: #selector(didTapOverlay), for: .touchUpInside) + self.presentingController = presentingController + + context.containerView.addSubview(presentedView) + presentedView.translatesAutoresizingMaskIntoConstraints = false + + presentedConstraints = [ + presentedView.topAnchor.constraint(equalTo: context.containerView.topAnchor), + presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), + presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), + presentedView.bottomAnchor.constraint(equalTo: context.containerView.bottomAnchor) + ] + + dismissedConstraints = [ + presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), + presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), + presentedView.topAnchor.constraint(equalTo: context.containerView.bottomAnchor), + presentedView.heightAnchor.constraint(equalTo: context.containerView.heightAnchor) + ] + + NSLayoutConstraint.activate(dismissedConstraints) + + context.containerView.setNeedsLayout() + context.containerView.layoutIfNeeded() + + NSLayoutConstraint.deactivate(dismissedConstraints) + NSLayoutConstraint.activate(presentedConstraints) + + UIView.animate( + withDuration: transitionDuration(using: context), + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: .curveEaseInOut, + animations: { + darkOverlayView.alpha = 1.0 + context.containerView.setNeedsLayout() + context.containerView.layoutIfNeeded() + }, + completion: { _ in + context.completeTransition(true) + }) + } + + private func dismiss(using context: UIViewControllerContextTransitioning) { + NSLayoutConstraint.deactivate(presentedConstraints) + NSLayoutConstraint.activate(dismissedConstraints) + + UIView.animate( + withDuration: transitionDuration(using: context), + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: .curveEaseInOut, + animations: { [weak darkOverlayView] in + darkOverlayView?.alpha = 0.0 + context.containerView.setNeedsLayout() + context.containerView.layoutIfNeeded() + }, + completion: { [weak self] _ in + context.completeTransition(true) + self?.onDismissal() + }) + } + + @objc private func didTapOverlay() { + if let presentingController { + presentingController.dismiss(animated: true) + } + } +} diff --git a/Sources/AppNavigation/PresentNickname.swift b/Sources/AppNavigation/PresentNickname.swift index 80121bbfc2c06ce41a88775adf0af3613f8b44bd..96bb9295b0c44d11d0624871e6eae98fb6fdabd1 100644 --- a/Sources/AppNavigation/PresentNickname.swift +++ b/Sources/AppNavigation/PresentNickname.swift @@ -1,4 +1,5 @@ import UIKit +import ScrollViewController /// Opens up `Nickname` on a given parent view controller public struct PresentNickname: Action { @@ -35,27 +36,31 @@ public struct PresentNickname: Action { /// Performs `PresentNickname` action public struct PresentNicknameNavigator: TypedNavigator { /// Custom transitioning delegate - let transitioningDelegate = BottomTransitioningDelegate() + let transitioningDelegate = FullscreenTransitioningDelegate() /// View controller which should be opened up var viewController: (String, @escaping (String) -> Void) -> UIViewController /// - Parameters: /// - viewController: view controller which should be presented - public init(_ viewController: @escaping ( - String, @escaping (String) -> Void - ) -> UIViewController - ) { + public init(_ viewController: @escaping (String, @escaping (String) -> Void) -> UIViewController) { self.viewController = viewController } public func perform(_ action: PresentNickname, completion: @escaping () -> Void) { + let scrollViewController = ScrollViewController() let controller = viewController(action.prefilled, action.completion) + scrollViewController.addChild(controller) + scrollViewController.contentView = controller.view + scrollViewController.wrapperView.handlesTouchesOutsideContent = true + scrollViewController.wrapperView.alignContentToBottom = true + scrollViewController.scrollView.bounces = false + controller.didMove(toParent: scrollViewController) controller.transitioningDelegate = transitioningDelegate - controller.modalPresentationStyle = .overFullScreen + scrollViewController.modalPresentationStyle = .overFullScreen action.parent.present( - controller, + scrollViewController, animated: action.animated, completion: completion )