nathantannar4 / Transmission

Bridges UIKit presentation APIs to a SwiftUI API so you can use presentation controllers, interactive transitions and more.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Possible to only provide a custom animator?

pixelmatrix opened this issue · comments

I'm looking to simply control the animation curves of the sheet presentation, rather than the presentation itself. In UIKit you can do this by returning nil for the UIPresentationController, or omitting presentationController(forPresented:presenting:source:) in your UIViewControllerTransitioningDelegate.

This library offers the ability to provide an animator through a custom PresentationLinkTransition, but in order to customize the animation you must also provide the presentation controller. Looking at how much setup there is for the .sheet transition's UIPresentationController, I'd prefer to avoid this.

A couple of ideas come to mind here:

  1. Could the presentation / dismissal animators be somehow provided as a sheet option?
  2. Could the PresentationLinkCustomTransition allow you to return a nil UIPresentationController to use the implementation from sheet instead?

Can you please add an example of how you would customize the sheet presentation duration in UIKit? I myself have not done this before, and simply adding UIView.animate to call present does not work.

Ah, sure. You need to provide a custom animator object from your UIViewControllerTransitioningDelegate, without customizing the UIPresentationController. Here's an example:

import UIKit
import SwiftUI

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    let button = UIButton()
    button.setTitle("Show Sheet", for: .normal)
    view.addSubview(button)
    button.sizeToFit()
    button.center = view.center
    button.setTitleColor(.systemBlue, for: .normal)
    button.addTarget(self, action: #selector(showSheet), for: .primaryActionTriggered)
  }
  
  @objc private func showSheet() {
    let vc = UIViewController()
    vc.view.backgroundColor = .systemBackground
    vc.transitioningDelegate = self
    vc.sheetPresentationController?.detents = [.medium()]
    present(vc, animated: true)
  }

}

extension ViewController: UIViewControllerTransitioningDelegate {
  
  func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    let animator = CoverSpringTransitionAnimator()
    animator.isPresenting = false
    return animator
  }

  func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    CoverTransitionAnimator(duration: 1)
  }

}

open class CoverTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    public var isPresenting = true
    public var duration = 0.5

    public override init() {
        super.init()
    }

    public init(duration: TimeInterval) {
        super.init()
        self.duration = duration
    }

    /// Customization point for sub-classes to customize animation parameters
    open func animate(animations: @escaping () -> Void, completion: @escaping () -> Void) {
      UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: animations) { _ in
          completion()
      }
    }

    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let container = transitionContext.containerView
        let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
        let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)

        let fromFrame = transitionContext.viewController(forKey: .from).map { transitionContext.finalFrame(for: $0) } ?? container.bounds

        let toFrame = transitionContext.viewController(forKey: .to).map { transitionContext.finalFrame(for: $0) } ?? container.bounds

        let completion = {
            transitionContext.completeTransition(true)
        }

        if isPresenting {
          performPresentingTransition(
            animated: transitionContext.isAnimated,
            containerView: container,
            fromView: fromView,
            toView: toView,
            targetFrame: toFrame,
            completion: completion
          )
        } else {
          performDismissalTransition(
            animated: transitionContext.isAnimated,
            containerView: container,
            fromView: fromView,
            toView: toView,
            targetFrame: fromFrame,
            completion: completion
          )
        }
    }

    public func performPresentingTransition(animated: Bool, containerView: UIView, fromView: UIView?, toView: UIView?, targetFrame: CGRect, completion: @escaping () -> Void) {
        // transitioning view is toView
        toView?.frame = targetFrame
        toView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)

        if let fromView {
            containerView.addSubview(fromView)
        }
        if let toView {
            containerView.addSubview(toView)
        }
        let animations: () -> Void = {
            toView?.transform = CGAffineTransform.identity
        }

        if animated {
            animate(animations: animations, completion: completion)
        } else {
            animations()
            completion()
        }
    }

    public func performDismissalTransition(animated: Bool, containerView: UIView, fromView: UIView?, toView: UIView?, targetFrame: CGRect, completion: @escaping () -> Void) {
        // transitioning view is fromView
        if let toView {
            containerView.addSubview(toView)
        }
        if let fromView {
            containerView.addSubview(fromView)
        }

        let animations: () -> Void = {
            fromView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
        }

        if animated {
            animate(animations: animations, completion: completion)
        } else {
            animations()
            completion()
        }
    }

    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }
}

#Preview {
  ViewController()
}

This should work automatically with Transmission already. Please let me know if there is any functionality that is not working, but from some quick testing it looks to work as expected.

PresentationLink(
    transition: .custom(CustomTransition())
) {
    // ..
} label: {
    Text("PresentationLink")
}

struct CustomTransition: PresentationLinkCustomTransition {
  func presentationController(
      sourceView: UIView,
      presented: UIViewController,
      presenting: UIViewController?
  ) -> UIPresentationController {
      UISheetPresentationController(
          presentedViewController: presented,
          presenting: presenting
      )
  }

  func animationController(
      forDismissed dismissed: UIViewController
  ) -> UIViewControllerAnimatedTransitioning? {
      let animator = CoverTransitionAnimator(duration: 1)
      animator.isPresenting = false
      return animator
  }

  func animationController(
      forPresented presented: UIViewController,
      presenting: UIViewController
  ) -> UIViewControllerAnimatedTransitioning? {
      CoverTransitionAnimator(duration: 1)
  }
}
class CoverTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {

        let isPresenting: Bool
        let duration: TimeInterval

        var animator: UIViewPropertyAnimator?

        init(isPresenting: Bool, duration: TimeInterval) {
            self.isPresenting = isPresenting
            self.duration = duration
            super.init()
        }

        func transitionDuration(
            using transitionContext: UIViewControllerContextTransitioning?
        ) -> TimeInterval {
            return transitionContext?.isAnimated == true ? duration : 0
        }

        func animateTransition(
            using transitionContext: UIViewControllerContextTransitioning
        ) {
            let animator = makeAnimatorIfNeeded(using: transitionContext)
            animator.startAnimation()

            if !transitionContext.isAnimated {
                animator.stopAnimation(false)
                animator.finishAnimation(at: .end)
            }
        }

        func interruptibleAnimator(
            using transitionContext: UIViewControllerContextTransitioning
        ) -> UIViewImplicitlyAnimating {
            makeAnimatorIfNeeded(using: transitionContext)
        }

        func animationEnded(_ transitionCompleted: Bool) {
            animator = nil
        }

        func makeAnimatorIfNeeded(
            using transitionContext: UIViewControllerContextTransitioning
        ) -> UIViewPropertyAnimator {
            if let animator = animator {
                return animator
            }

            let animator = UIViewPropertyAnimator(
                duration: duration,
                curve: .linear
            )


            let containerView = transitionContext.containerView
            let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
            let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)

            let fromFrame = transitionContext.viewController(forKey: .from).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds
            let toFrame = transitionContext.viewController(forKey: .to).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds

            if isPresenting {
                // transitioning view is toView
                toView?.frame = toFrame
                toView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)

                if let fromView {
                    containerView.addSubview(fromView)
                }
                if let toView {
                    containerView.addSubview(toView)
                }

                animator.addAnimations {
                    toView?.transform = CGAffineTransform.identity
                }
            } else {
                // transitioning view is fromView
                if let toView {
                    containerView.addSubview(toView)
                }
                if let fromView {
                    containerView.addSubview(fromView)
                }

                animator.addAnimations {
                    fromView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
                }
            }

            animator.addCompletion { animatingPosition in
                switch animatingPosition {
                case .end:
                    transitionContext.completeTransition(true)
                default:
                    transitionContext.completeTransition(false)
                }
            }
            self.animator = animator
            return animator
        }
    }

Ah, okay. This seems to be working for me now. Thanks!