nathantannar4 / Transmission

Bridges UIKit presentation APIs to a SwiftUI API so you can use presentation controllers, interactive transitions and more.
BSD 2-Clause "Simplified" License
414 stars 15 forks source link

Slide transition detents #60

Closed coratype closed 1 week ago

coratype commented 3 weeks ago

Is there a simple way to make slide transitions similar to sheets with detents instead of making them full screen? The transition would basically be exactly the same it would just come in from the top, left, right?

coratype commented 3 weeks ago

Also on a side note, is it possible for the presenting view controller to be "above" the presented view controller?

nathantannar4 commented 3 weeks ago

Also on a side note, is it possible for the presenting view controller to be "above" the presented view controller?

So you want to present a view behind another view? Curious to hear more about that use case. In theory that may be possible with a custom UIKit presentation controller but I'd imagine you would run into other issues

nathantannar4 commented 3 weeks ago

Is there a simple way to make slide transitions similar to sheets with detents instead of making them full screen? The transition would basically be exactly the same it would just come in from the top, left, right?

No simple way, as the current .slide transition does not support detents. You would have to create your own custom transition

coratype commented 2 weeks ago

sorry i got side tracked

i actually created a sample of the existing slide that something close to what i want, not perfect but the idea is there. also it can be done in like 3 lines of swiftui code with tabview so

//
// Copyright (c) Nathan Tannar
//

#if os(iOS)

import SwiftUI
import UIKit

@available(iOS 14.0, *)
class SlidePresentationController: InteractivePresentationController {
    var edge: Edge

    override var edges: Edge.Set {
        get { Edge.Set(edge) }
        set {}
    }

    override var wantsInteractiveDismissal: Bool {
        return false
    }

    override var presentationStyle: UIModalPresentationStyle {
        .custom
    }

    private var depth = 0 {
        didSet {
            layoutPresentedView(frame: frameOfPresentedViewInContainerView)
            dimmingView.alpha = depth > 0 ? 0 : 1
        }
    }

    private var prevPresentationController: SlidePresentationController? {
        presentingViewController.presentationController as? SlidePresentationController
    }

    init(
        edge: Edge = .bottom,
        presentedViewController: UIViewController,
        presenting presentingViewController: UIViewController?
    ) {
        self.edge = edge
        super.init(
            presentedViewController: presentedViewController,
            presenting: presentingViewController
        )
    }

    private func push() {
        prevPresentationController?.depth += 1
        prevPresentationController?.prevPresentationController?.depth += 1
    }

    private func pop() {
        prevPresentationController?.depth -= 1
        prevPresentationController?.prevPresentationController?.depth -= 1
    }

    override var frameOfPresentedViewInContainerView: CGRect {
        guard let containerView = containerView else { return .zero }

        let desiredWidth: CGFloat = containerView.bounds.width * 0.96

        let frame = CGRect(
            x: containerView.bounds.width - desiredWidth,
            y: containerView.bounds.origin.y,
            width: desiredWidth,
            height: containerView.bounds.height
        )

        return frame
    }

    override func presentationTransitionWillBegin() {
        super.presentationTransitionWillBegin()

        // Set up the presented view's appearance
        presentedViewController.view.layer.masksToBounds = true
        presentedViewController.view.layer.cornerCurve = .continuous

        // Dimming view setup
        dimmingView.backgroundColor = UIColor.white.withAlphaComponent(0.1)
        dimmingView.alpha = 0
        dimmingView.isHidden = false
        dimmingView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissPresentedViewController)))

        // Make sure the containerView and presenting view are available
        guard let containerView = containerView, let presentingView = presentingViewController.view else { return }

        // Insert dimming view above the presenting view
        presentingViewController.view.insertSubview(dimmingView, aboveSubview: presentingView)

        // Set the dimming view's constraints relative to the presenting view
        dimmingView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            dimmingView.topAnchor.constraint(equalTo: presentingView.topAnchor),
            dimmingView.bottomAnchor.constraint(equalTo: presentingView.bottomAnchor),
            dimmingView.leftAnchor.constraint(equalTo: presentingView.leftAnchor),
            dimmingView.rightAnchor.constraint(equalTo: presentingView.rightAnchor)
        ])

        // Animate the dimming view alongside the presentation
        if let transitionCoordinator = presentedViewController.transitionCoordinator {
            transitionCoordinator.animate(alongsideTransition: { [unowned self] _ in
                self.dimmingView.alpha = 1
                self.push()
            }, completion: { [unowned self] ctx in
                self.dimmingView.alpha = ctx.isCancelled ? 0 : 1
                if ctx.isCancelled {
                    self.pop()
                }
            })
        }
    }

    override func presentedViewTransform(for translation: CGPoint) -> CGAffineTransform {
        return .identity
    }

    override func containerViewDidLayoutSubviews() {
        super.containerViewDidLayoutSubviews()

        presentingViewController.view.isHidden = presentedViewController.presentedViewController != nil
    }

    @objc
    private func dismissPresentedViewController() {
        presentedViewController.dismiss(animated: true)
    }
}

@available(iOS 14.0, *)
class SlideTransition: PresentationControllerTransition {
    let options: PresentationLinkTransition.SlideTransitionOptions

    static let displayCornerRadius: CGFloat = {
        return max(UIScreen.main.displayCornerRadius, 12)
    }()

    init(
        isPresenting: Bool,
        options: PresentationLinkTransition.SlideTransitionOptions,
        animation: Animation?
    ) {
        self.options = options
        super.init(isPresenting: isPresenting, animation: animation)
    }

    override func transitionAnimator(
        using transitionContext: UIViewControllerContextTransitioning
    ) -> UIViewPropertyAnimator {
        let isPresenting = isPresenting
        let animator = UIViewPropertyAnimator(animation: animation) ?? UIViewPropertyAnimator(duration: duration, curve: completionCurve)

        guard
            let presented = transitionContext.viewController(forKey: isPresenting ? .to : .from),
            let presenting = transitionContext.viewController(forKey: isPresenting ? .from : .to)
        else {
            transitionContext.completeTransition(false)
            return animator
        }

        let safeAreaInsets = transitionContext.containerView.safeAreaInsets
        let cornerRadius = options.preferredCornerRadius ?? Self.displayCornerRadius

        var dzTransform = CGAffineTransform(scaleX: 0.92, y: 0.92)
        switch options.edge {
        case .top:
            dzTransform = dzTransform.translatedBy(x: 0, y: safeAreaInsets.bottom / 2)
        case .bottom:
            dzTransform = dzTransform.translatedBy(x: 0, y: safeAreaInsets.top / 2)
        case .leading:
            switch presented.traitCollection.layoutDirection {
            case .rightToLeft:
                dzTransform = dzTransform.translatedBy(x: 0, y: safeAreaInsets.left / 2)
            default:
                dzTransform = dzTransform.translatedBy(x: 0, y: safeAreaInsets.right / 2)
            }
        case .trailing:
            switch presented.traitCollection.layoutDirection {
            case .leftToRight:
                dzTransform = dzTransform.translatedBy(x: -safeAreaInsets.bottom * 0.55, y: safeAreaInsets.right / 2)
            default:
                dzTransform = dzTransform.translatedBy(x: 0, y: safeAreaInsets.left / 2)
            }
        }

        print("safeAreaInsets: \(safeAreaInsets)")

        presented.view.layer.masksToBounds = true
        presented.view.layer.cornerCurve = .continuous

        presenting.view.layer.masksToBounds = true
        presenting.view.layer.cornerCurve = .continuous

        let frame = transitionContext.finalFrame(for: presented)
        if isPresenting {
            transitionContext.containerView.addSubview(presented.view)
            presented.view.frame = frame
            presented.view.transform = presentationTransform(
                presented: presented,
                frame: frame
            )
        } else {
            presented.view.layer.cornerRadius = cornerRadius
        }

        animator.addAnimations {
            if isPresenting {
                presenting.view.layer.cornerRadius = cornerRadius / 3
                presenting.view.transform = dzTransform

                presented.view.transform = .identity
                presented.view.layer.cornerRadius = cornerRadius * 0.75
            } else {
                presented.view.transform = self.presentationTransform(
                    presented: presented,
                    frame: frame
                )
                presented.view.layer.cornerRadius = 0

                presenting.view.transform = .identity
                presenting.view.layer.cornerRadius = cornerRadius
            }
        }
        animator.addCompletion { animatingPosition in
            switch animatingPosition {
            case .end:
                transitionContext.completeTransition(true)
            default:
                transitionContext.completeTransition(false)
            }
        }
        return animator
    }

    private func presentationTransform(
        presented: UIViewController,
        frame: CGRect
    ) -> CGAffineTransform {
        switch options.edge {
        case .top:
            return CGAffineTransform(translationX: 0, y: -frame.maxY)
        case .bottom:
            return CGAffineTransform(translationX: 0, y: frame.maxY)
        case .leading:
            switch presented.traitCollection.layoutDirection {
            case .rightToLeft:
                return CGAffineTransform(translationX: frame.maxX, y: 0)
            default:
                return CGAffineTransform(translationX: -frame.maxX, y: 0)
            }
        case .trailing:
            switch presented.traitCollection.layoutDirection {
            case .leftToRight:
                return CGAffineTransform(translationX: frame.maxX, y: 0)
            default:
                return CGAffineTransform(translationX: -frame.maxX, y: 0)
            }
        }
    }
}

#endif