I have the following code:
import UIKit
import SwiftUI
class ViewController: UIViewController {
#IBOutlet weak var testButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBOutlet weak var swiftuiButton: UIButton!
#IBAction func didTap(_ sender: Any) {
let storyboard = UIStoryboard(name: "DetailsVC", bundle: .main)
let vc = storyboard.instantiateInitialViewController() as! DetailsVC
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = self
present(vc, animated: true)
}
#IBAction func swiftUITest(_ sender: Any) {
let swiftUIView = SwiftUIView() {
self.dismiss(animated: true)
}
let hostingController = UIHostingController.init(rootView: swiftUIView)
hostingController.modalPresentationStyle = .custom
hostingController.transitioningDelegate = self
present(hostingController, animated: true)
}
}
struct SwiftUIView: View {
var didTapClose:()->()
var body: some View {
GeometryReader { geo in
ZStack {
Color.yellow.ignoresSafeArea()
Text("SWIFTUI VIEW")
.foregroundColor(.black)
}
.transaction { transaction in
transaction.animation = nil
}
.frame(width: geo.size.width, height: geo.size.height)
.border(.red)
.onTapGesture {
didTapClose()
}
}
}
}
extension ViewController:UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return ToTileDetailsAnimator(fromFrame: testButton.frame, duration: 5)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return nil
}
}
final class ToTileDetailsAnimator:NSObject, UIViewControllerAnimatedTransitioning {
fileprivate(set) var fromFrame:CGRect
fileprivate(set) var duration:Double
init(fromFrame:CGRect, duration:Double) {
self.fromFrame = fromFrame
self.duration = duration
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from) else { return }
let finalFrame = fromVC.view.frame.insetBy(dx: 20, dy: 20)
guard let toVC = transitionContext.viewController(forKey: .to) else { return }
let containerView = transitionContext.containerView
containerView.addSubview(toVC.view)
toVC.view.frame = fromFrame
toVC.view.clipsToBounds = true
toVC.view.layer.cornerRadius = 6
toVC.view.layoutIfNeeded()
fromVC.beginAppearanceTransition(false, animated: true)
toVC.beginAppearanceTransition(true, animated: true)
toVC.view.alpha = 0
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut, animations: {
toVC.view.frame = finalFrame
toVC.view.alpha = 1
toVC.view.layoutIfNeeded()
}) { (completed) in
toVC.view.frame = finalFrame
toVC.view.layoutIfNeeded()
toVC.view.alpha = 1
fromVC.endAppearanceTransition()
toVC.endAppearanceTransition()
let transitionSuccess = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(transitionSuccess)
}
}
}
Issue:
When presenting a UIViewController all works great. When presenting a UIHostingController the transition animation shows the SwiftUIView NOT growing and like the UIKit case correctly does.
Why is the SwiftUIView not scaling up during the animation? Why is it at full size immediately?
This is what the transition animations look like:
Related
I want to enable interactive modal dismissal that pans along with a users finger on a fullscreen modally presented view controller .fullscreen.
I've seen that it's fairly trivial to do so on the .pageSheet and the .formSheet which have it built in but have not seen a clear example for the full screen.
I'm guessing I'd need to have a pan gesture added to my vc within the body of it's code and then adjust for the states myself but wondering if anyone knows what exactly needs to be done / if there's a simpler way to do it as it seems much more complicated for the .fullscreen case
It can be done with creating your custom UIPresentationController and UIViewControllerTransitioningDelegate. Lets say we have TestViewController and we want to present SecondViewController with total presentedHeight of 1.0 (fullScreen). Presentation will be triggered with #IBAction func buttonPressed and can be dismissed by dragging controller down (as we are used to it). It would be also nice to add some backgroundEffect to be gradually changed while user is sliding down the SecondViewController (especially when used only presentedHeight of 0.6).
Firstly we define OverlayViewController which will be later superclass of presented SecondViewControllerand will contain UIPanGestureRecognizer.
class OverlayViewController: UIViewController {
var hasSetPointOrigin = false
var pointOrigin: CGPoint?
var delegate: OverlayViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction))
view.addGestureRecognizer(panGesture)
}
override func viewDidLayoutSubviews() {
if !hasSetPointOrigin {
hasSetPointOrigin = true
pointOrigin = self.view.frame.origin
}
}
#objc func panGestureRecognizerAction(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
// Not allowing the user to drag the view upward
guard translation.y >= 0 else { return }
let currentPosition = translation.y
let originPos = self.pointOrigin
delegate?.userDragged(draggedPercentage: translation.y/originPos!.y)
// setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down
view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)
if sender.state == .ended {
let dragVelocity = sender.velocity(in: view)
if dragVelocity.y >= 1100 {
self.dismiss(animated: true, completion: nil)
} else {
// Set back to original position of the view controller
UIView.animate(withDuration: 0.3) {
self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
self.delegate?.animateBlurBack(seconds: 0.3)
}
}
}
}
}
protocol OverlayViewDelegate: AnyObject {
func userDragged(draggedPercentage: CGFloat)
func animateBlurBack(seconds: TimeInterval)
}
Next we define custom PresentationController
class PresentationController: UIPresentationController {
private var backgroundEffectView: UIView?
private var backgroundEffect: BackgroundEffect?
private var viewHeight: CGFloat?
private let maxDim:CGFloat = 0.6
private var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
convenience init(presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?,
backgroundEffect: BackgroundEffect = .blur,
viewHeight: CGFloat = 0.6)
{
self.init(presentedViewController: presentedViewController, presenting: presentingViewController)
self.backgroundEffect = backgroundEffect
self.backgroundEffectView = returnCorrectEffectView(backgroundEffect)
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
self.backgroundEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.backgroundEffectView?.isUserInteractionEnabled = true
self.backgroundEffectView?.addGestureRecognizer(tapGestureRecognizer)
self.viewHeight = viewHeight
}
private override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * (1-viewHeight!)),
size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
viewHeight!))
}
override func presentationTransitionWillBegin() {
self.backgroundEffectView?.alpha = 0
self.containerView?.addSubview(backgroundEffectView!)
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}, completion: { (UIViewControllerTransitionCoordinatorContext) in })
}
override func dismissalTransitionWillBegin() {
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.alpha = 0
}, completion: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.removeFromSuperview()
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
backgroundEffectView?.frame = containerView!.bounds
}
#objc func dismissController(){
self.presentedViewController.dismiss(animated: true, completion: nil)
}
func graduallyChangeOpacity(withPercentage: CGFloat) {
self.backgroundEffectView?.alpha = withPercentage
}
func returnCorrectEffectView(_ effect: BackgroundEffect) -> UIView {
switch effect {
case .blur:
var blurEffect = UIBlurEffect(style: .dark)
if self.traitCollection.userInterfaceStyle == .dark {
blurEffect = UIBlurEffect(style: .light)
}
return UIVisualEffectView(effect: blurEffect)
case .dim:
var dimView = UIView()
dimView.backgroundColor = .black
if self.traitCollection.userInterfaceStyle == .dark {
dimView.backgroundColor = .gray
}
dimView.alpha = maxDim
return dimView
case .none:
let clearView = UIView()
clearView.backgroundColor = .clear
return clearView
}
}
}
extension PresentationController: OverlayViewDelegate {
func userDragged(draggedPercentage: CGFloat) {
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
switch self.backgroundEffect! {
case .blur:
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
case .dim:
graduallyChangeOpacity(withPercentage: maxDim-draggedPercentage)
case .none:
self.backgroundEffectView?.alpha = 0
}
}
func animateBlurBack(seconds: TimeInterval) {
UIView.animate(withDuration: seconds) {
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}
}
}
enum BackgroundEffect {
case blur
case dim
case none
}
Create SecondViewController subclassing OverlayViewController:
class SecondViewController: OverlayViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
addSlider()
}
func addSlider() {
let sliderWidth:CGFloat = 100
let centerOfScreen = self.view.frame.size.width / 2
let rect = CGRect(x: centerOfScreen - sliderWidth/2, y: 80, width: sliderWidth, height: 10)
let slider = UIView(frame: rect)
slider.backgroundColor = .black
self.view.addSubview(slider)
}
Add showOverlay() function that will be triggered after buttonPressed and conform your presenting UIViewController (TestViewController) to UIViewControllerTransitioningDelegate :
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func buttonPressed(_ sender: Any) {
showOverlay()
}
func showOverlay() {
let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "secondVC") as! SecondViewController
secondVC.modalPresentationStyle = .custom
secondVC.transitioningDelegate = self
self.present(secondVC, animated: true, completion: nil)
}
}
extension TestViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController?
{
let presentedHeight: CGFloat = 1.0
let controller = PresentationController(presentedViewController: presented,
presenting: presenting,
backgroundEffect: .dim,
viewHeight: presentedHeight)
if let vc = presented as? OverlayViewController {
vc.delegate = controller
}
return controller
}
}
Now we should be able to present SecondViewController with showOverlay() function setting its presentedHeight to 1.0 and .dim background effect. We can dismiss SecondViewController similar to another modal presentations.
So, straight to the problem:
I've created a custom UIViewControllerTransitioningDelegate that I use to animate a view from one view controller, to full-screen in another view controller. Im doing this by creating UIViewControllerAnimatedTransitioning-objects that animate the presented view's frame. And it works great! Except when I try to adjust the additionalSafeAreaInsets of the view controller owning the view during dismissal...
It looks like this property is not accounted for when I'm trying to animate the dismissal of the view controller and its view. It works fine during presentation.
The gif below shows how it looks. The red box is the safe area (plus some padding) of the presented view - which I'm trying to compensate for during animation, using the additionalSafeAreaInsets property of the view controller owning the view.
As the gif shows, the safe area is properly adjusted during presentation but not during dismissal.
So, what I want is: use additionalSafeAreaInsets to diminish the effect of the safe area during animation, by setting additionalSafeAreaInsets to the "inverted" values of the safe area. So that the effective safe area starts at 0 and "animates" to the expected value during presentation, and starts at expected value and "animates" to 0 during dismissal.
(I'm quoting "animates", since its actually the view's frame that is animated. But UIKit/Auto Layout use these properties when calculating the frames)
Any thoughts on how to battle this issue is great welcome!
The code for the custom UIViewControllerTransitioningDelegate is provided below.
//
// FullScreenTransitionManager.swift
//
import Foundation
import UIKit
// MARK: FullScreenPresentationController
final class FullScreenPresentationController: UIPresentationController {
private let backgroundView: UIView = {
let view = UIView()
view.backgroundColor = .systemBackground
view.alpha = 0
return view
}()
private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
#objc private func onTap(_ gesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true)
}
}
// MARK: UIPresentationController
extension FullScreenPresentationController {
override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
containerView.addGestureRecognizer(tapGestureRecognizer)
containerView.addSubview(backgroundView)
backgroundView.frame = containerView.frame
guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
transitionCoordinator.animate(alongsideTransition: { context in
self.backgroundView.alpha = 1
})
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if !completed {
backgroundView.removeFromSuperview()
containerView?.removeGestureRecognizer(tapGestureRecognizer)
}
}
override func dismissalTransitionWillBegin() {
guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
transitionCoordinator.animate(alongsideTransition: { context in
self.backgroundView.alpha = 0
})
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed {
backgroundView.removeFromSuperview()
containerView?.removeGestureRecognizer(tapGestureRecognizer)
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
guard
let containerView = containerView,
let presentedView = presentedView
else { return }
coordinator.animate(alongsideTransition: { context in
self.backgroundView.frame = containerView.frame
presentedView.frame = self.frameOfPresentedViewInContainerView
})
}
}
// MARK: FullScreenTransitionManager
final class FullScreenTransitionManager: NSObject, UIViewControllerTransitioningDelegate {
private weak var anchorView: UIView?
init(anchorView: UIView) {
self.anchorView = anchorView
}
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
FullScreenPresentationController(presentedViewController: presented, presenting: presenting)
}
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let anchorFrame = anchorView?.safeAreaLayoutGuide.layoutFrame ?? CGRect(origin: presented.view.center, size: .zero)
return FullScreenAnimationController(animationType: .present,
anchorFrame: anchorFrame)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let anchorFrame = anchorView?.safeAreaLayoutGuide.layoutFrame ?? CGRect(origin: dismissed.view.center, size: .zero)
return FullScreenAnimationController(animationType: .dismiss,
anchorFrame: anchorFrame)
}
}
// MARK: UIViewControllerAnimatedTransitioning
final class FullScreenAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum AnimationType {
case present
case dismiss
}
private let animationType: AnimationType
private let anchorFrame: CGRect
private let animationDuration: TimeInterval
private var propertyAnimator: UIViewPropertyAnimator?
init(animationType: AnimationType, anchorFrame: CGRect, animationDuration: TimeInterval = 0.3) {
self.animationType = animationType
self.anchorFrame = anchorFrame
self.animationDuration = animationDuration
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
animationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
switch animationType {
case .present:
guard
let toViewController = transitionContext.viewController(forKey: .to)
else {
return transitionContext.completeTransition(false)
}
transitionContext.containerView.addSubview(toViewController.view)
propertyAnimator = presentAnimator(with: transitionContext, animating: toViewController)
case .dismiss:
guard
let fromViewController = transitionContext.viewController(forKey: .from)
else {
return transitionContext.completeTransition(false)
}
propertyAnimator = dismissAnimator(with: transitionContext, animating: fromViewController)
}
}
private func presentAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
let finalFrame = transitionContext.finalFrame(for: viewController)
let safeAreaInsets = transitionContext.containerView.safeAreaInsets
let safeAreaCompensation = UIEdgeInsets(top: -safeAreaInsets.top,
left: -safeAreaInsets.left,
bottom: -safeAreaInsets.bottom,
right: -safeAreaInsets.right)
viewController.additionalSafeAreaInsets = safeAreaCompensation
viewController.view.frame = anchorFrame
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut, .layoutSubviews], animations: {
viewController.additionalSafeAreaInsets = .zero
viewController.view.frame = finalFrame
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
private func dismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
let finalFrame = anchorFrame
let safeAreaInsets = transitionContext.containerView.safeAreaInsets
let safeAreaCompensation = UIEdgeInsets(top: -safeAreaInsets.top,
left: -safeAreaInsets.left,
bottom: -safeAreaInsets.bottom,
right: -safeAreaInsets.right)
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut, .layoutSubviews], animations: {
viewController.additionalSafeAreaInsets = safeAreaCompensation
viewController.view.frame = finalFrame
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
After some debugging I managed to find a workaround to this problem.
In short, it looks like the safe area is not updated after UIViewController.viewWillDisappear is called, and hence any changes to .additionalSafeAreaInsets is ignored (since these insets modifies the safe area of the view controller's view).
My current workaround is somewhat hacky, but it gets the job done. Since UIViewControllerTransitioningDelegate.animationController(forDismissed...) is called right before UIViewController.viewWillDisappear and UIViewControllerAnimatedTransitioning.animateTransition(using transitionContext...), I start the dismiss animation already in that method. That way the layout calculations for the animation get correct, and the correct safe area is set.
Below is the code for my custom UIViewControllerTransitioningDelegate with the workaround. Note: I've removed the use of .additionalSafeAreaInsets since its not necessary at all! And I've no idea why I thought I needed it in the first place...
//
// FullScreenTransitionManager.swift
//
import Foundation
import UIKit
// MARK: FullScreenPresentationController
final class FullScreenPresentationController: UIPresentationController {
private let backgroundView: UIView = {
let view = UIView()
view.backgroundColor = .systemBackground
view.alpha = 0
return view
}()
private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap))
#objc private func onTap(_ gesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true)
}
}
// MARK: UIPresentationController
extension FullScreenPresentationController {
override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
containerView.addGestureRecognizer(tapGestureRecognizer)
containerView.addSubview(backgroundView)
backgroundView.frame = containerView.frame
guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
transitionCoordinator.animate(alongsideTransition: { context in
self.backgroundView.alpha = 1
})
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if !completed {
backgroundView.removeFromSuperview()
containerView?.removeGestureRecognizer(tapGestureRecognizer)
}
}
override func dismissalTransitionWillBegin() {
guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return }
transitionCoordinator.animate(alongsideTransition: { context in
self.backgroundView.alpha = 0
})
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed {
backgroundView.removeFromSuperview()
containerView?.removeGestureRecognizer(tapGestureRecognizer)
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
guard let containerView = containerView else { return }
coordinator.animate(alongsideTransition: { context in
self.backgroundView.frame = containerView.frame
})
}
}
// MARK: FullScreenTransitionManager
final class FullScreenTransitionManager: NSObject, UIViewControllerTransitioningDelegate {
fileprivate enum AnimationState {
case present
case dismiss
}
private weak var anchorView: UIView?
private var animationState: AnimationState = .present
private var animationDuration: TimeInterval = Resources.animation.duration
private var anchorViewFrame: CGRect = .zero
private var propertyAnimator: UIViewPropertyAnimator?
init(anchorView: UIView) {
self.anchorView = anchorView
}
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
FullScreenPresentationController(presentedViewController: presented, presenting: presenting)
}
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
prepare(animationState: .present)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// Starting the animation here, since UIKit do not update safe area insets after UIViewController.viewWillDisappear() is called
defer {
propertyAnimator = dismissAnimator(animating: dismissed)
}
return prepare(animationState: .dismiss)
}
}
// MARK: UIViewControllerAnimatedTransitioning
extension FullScreenTransitionManager: UIViewControllerAnimatedTransitioning {
private func prepare(animationState: AnimationState,
animationDuration: TimeInterval = Resources.animation.duration) -> UIViewControllerAnimatedTransitioning? {
guard let anchorView = anchorView else { return nil }
self.animationState = animationState
self.animationDuration = animationDuration
self.anchorViewFrame = anchorView.safeAreaLayoutGuide.layoutFrame
return self
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
animationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
switch animationState {
case .present:
guard
let toViewController = transitionContext.viewController(forKey: .to)
else {
return transitionContext.completeTransition(false)
}
propertyAnimator = presentAnimator(with: transitionContext, animating: toViewController)
case .dismiss:
guard
let fromViewController = transitionContext.viewController(forKey: .from)
else {
return transitionContext.completeTransition(false)
}
propertyAnimator = updatedDismissAnimator(with: transitionContext, animating: fromViewController)
}
}
private func presentAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
transitionContext.containerView.addSubview(viewController.view)
let finalFrame = transitionContext.finalFrame(for: viewController)
viewController.view.frame = anchorViewFrame
viewController.view.layoutIfNeeded()
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: [.curveEaseInOut],
animations: {
viewController.view.frame = finalFrame
viewController.view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
private func dismissAnimator(animating viewController: UIViewController) -> UIViewPropertyAnimator {
let finalFrame = anchorViewFrame
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: animationDuration,
delay: 0,
options: [.curveEaseInOut],
animations: {
viewController.view.frame = finalFrame
viewController.view.layoutIfNeeded()
})
}
private func updatedDismissAnimator(with transitionContext: UIViewControllerContextTransitioning,
animating viewController: UIViewController) -> UIViewPropertyAnimator {
let propertyAnimator = self.propertyAnimator ?? dismissAnimator(animating: viewController)
propertyAnimator.addCompletion({ _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
self.propertyAnimator = propertyAnimator
return propertyAnimator
}
}
Also, here is a link to a Stack Overflow post regarding the safe area not updating after UIViewController.viewWillDisappear. And a link to a similar post on the Apple forums.
I am trying to make a pop over form the bottom of screen using UIPresentationController, so I followed raywenderlich guide here : https://www.raywenderlich.com/139277/uipresentationcontroller-tutorial-getting-started. I did the exact same thing, I only change the size and y position of the frame. The pop up consist of buttons that open the share sheet , but for some reason when I open the sheet then click "save to files", the "shave to files" view shows up and when I hit cancel my pop over goes full screen for a moment then changes to my custom size.
I tried to debug the app and found out that containerViewWillLayoutSubviews() doesn't get called untill the "save to file" view is dismissed. Anyone have an idea on how to solve this. Thank you
this is my code :
main :
final class MainViewController: UIViewController {
// MARK: - Properties
lazy var slideInTransitioningDelegate = SlideInPresentationManager()
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func showPopup(_ sender: Any) {
let controller = storyboard.instantiateViewController(withIdentifier: NSStringFromClass(MyPopUpController.self))
as! MyPopUpController
slideInTransitioningDelegate.direction = .bottom
slideInTransitioningDelegate.disableCompactHeight = true
controller.transitioningDelegate = slideInTransitioningDelegate
controller.modalPresentationStyle = .custom
}
mypopucontroller
final class MyPopUpController: UIViewController {
#IBAction func share(_ sender: Any) {
let activityController = UIActivityViewController(activityItems: ["message"], applicationActivities: nil)
present(activityController, animated: true)
}
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
slide in presentation controller :
final class SlideInPresentationController: UIPresentationController {
// MARK: - Properties
fileprivate var dimmingView: UIView!
private var direction: PresentationDirection
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
frame.size = size(forChildContentContainer: presentedViewController, withParentContainerSize: containerView!.bounds.size)
switch direction {
case .right:
frame.origin.x = containerView!.frame.width*(1.0/3.0)
case .bottom:
frame.origin.y = containerView!.frame.height*0.5
default:
frame.origin = .zero
}
return frame
}
// MARK: - Initializers
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, direction: PresentationDirection) {
self.direction = direction
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
setupDimmingView()
}
override func presentationTransitionWillBegin() {
containerView?.insertSubview(dimmingView, at: 0)
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 1.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1.0
})
}
override func dismissalTransitionWillBegin() {
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 0.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0.0
})
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
switch direction {
case .left, .right:
return CGSize(width: parentSize.width*(2.0/3.0), height: parentSize.height)
case .bottom, .top:
return CGSize(width: parentSize.width, height: parentSize.height*0.67)
}
}
}
// MARK: - Private
private extension SlideInPresentationController {
func setupDimmingView() {
dimmingView = UIView()
dimmingView.translatesAutoresizingMaskIntoConstraints = false
dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
dimmingView.alpha = 0.0
let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:)))
dimmingView.addGestureRecognizer(recognizer)
}
dynamic func handleTap(recognizer: UITapGestureRecognizer) {
presentingViewController.dismiss(animated: true)
}
}
slidein manager :
final class SlideInPresentationManager: NSObject {
// MARK: - Properties
var direction = PresentationDirection.left
var disableCompactHeight = false
}
// MARK: - UIViewControllerTransitioningDelegate
extension SlideInPresentationManager: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = SlideInPresentationController(presentedViewController: presented, presenting: presenting, direction: direction)
presentationController.delegate = self
return presentationController
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideInPresentationAnimator(direction: direction, isPresentation: true)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideInPresentationAnimator(direction: direction, isPresentation: false)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension SlideInPresentationManager: UIAdaptivePresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
if traitCollection.verticalSizeClass == .compact && disableCompactHeight {
return .overFullScreen
} else {
return .none
}
}
func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
guard case(.overFullScreen) = style else { return nil }
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "RotateViewController")
}
}
slidein animator:
final class SlideInPresentationAnimator: NSObject {
// MARK: - Properties
let direction: PresentationDirection
let isPresentation: Bool
// MARK: - Initializers
init(direction: PresentationDirection, isPresentation: Bool) {
self.direction = direction
self.isPresentation = isPresentation
super.init()
}
}
// MARK: - UIViewControllerAnimatedTransitioning
extension SlideInPresentationAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let key = isPresentation ? UITransitionContextViewControllerKey.to : UITransitionContextViewControllerKey.from
let controller = transitionContext.viewController(forKey: key)!
if isPresentation {
transitionContext.containerView.addSubview(controller.view)
}
let presentedFrame = transitionContext.finalFrame(for: controller)
var dismissedFrame = presentedFrame
switch direction {
case .left:
dismissedFrame.origin.x = -presentedFrame.width
case .right:
dismissedFrame.origin.x = transitionContext.containerView.frame.size.width
case .top:
dismissedFrame.origin.y = -presentedFrame.height
case .bottom:
dismissedFrame.origin.y = transitionContext.containerView.frame.size.height
}
let initialFrame = isPresentation ? dismissedFrame : presentedFrame
let finalFrame = isPresentation ? presentedFrame : dismissedFrame
let animationDuration = transitionDuration(using: transitionContext)
controller.view.frame = initialFrame
UIView.animate(withDuration: animationDuration, animations: {
controller.view.frame = finalFrame
}) { finished in
transitionContext.completeTransition(finished)
}
}
}
You can try to subclass UIPresentationController and override var presentedView: UIView? and enforce presentedView's frame.
override var presentedView: UIView? {
super.presentedView?.frame = frameOfPresentedViewInContainerView
return super.presentedView
}
See example: "Custom View Controller Presentation" from Kyle Bashour https://kylebashour.com/posts/custom-view-controller-presentation-tips
I am facing an incomprehensible problem I have a CollectionViewController and I want to make a custom animation.
My collection is a gallery and I want to switch from collection gallery. to fullscreen gallery.
So I have ControllerTransitionDelegate
extension NavigationGalleryViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return DimmingPresentationController(presentedViewController: presented, presenting: presenting)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let selectedCellFrame = self.collectionView?.cellForItem(at: IndexPath(item: index, section: 0))?.frame else { return nil }
return PresentingAnimator(pageIndex: index, originFrame: selectedCellFrame)
}
My DimmingPresentationController
class DimmingPresentationController: UIPresentationController {
lazy var background = UIView(frame: .zero)
override var shouldRemovePresentersView: Bool {
return false
}
override func presentationTransitionWillBegin() {
setupBackground()
// Grabing the coordinator responsible for the presentation so that the background can be animated at the same rate
if let coordinator = presentedViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { (_) in
self.background.alpha = 1
}, completion: nil)
}
}
private func setupBackground() {
background.backgroundColor = UIColor.black
background.autoresizingMask = [.flexibleWidth, .flexibleHeight]
background.frame = containerView!.bounds
containerView!.insertSubview(background, at: 0)
background.alpha = 0
}
}
And my presenting animator
class PresentingAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private let indexPath: IndexPath
private let originFrame: CGRect
private let duration: TimeInterval = 0.5
init(pageIndex: Int, originFrame: CGRect) {
self.indexPath = IndexPath(item: pageIndex, section: 0)
self.originFrame = originFrame
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from) as? NavigationGalleryViewController, // The problem is here !
let fromView = fromVC.collectionView?.cellForItem(at: indexPath) as? InstallationViewCell
else {
transitionContext.completeTransition(true)
return
}
// All the animation things
}
My BIG problem is that my execution go inside the else because he can't find the FromVC from the transitionContext.viewController.
And here is how I call my Gallery
gallery = SwiftPhotoGallery(delegate: self, dataSource: self)
// Gallery visual colours stuff
gallery.modalPresentationStyle = .custom
gallery.transitioningDelegate = self
present(gallery, animated: true, completion: { () -> Void in
self.gallery.currentPage = self.index
})
}
This is what I receive from the transitionContext :
Why the transitionContext won't give me the right VC ?
Well, I checked and noticed that transitionContext.viewController(forKey: .from) is NavigationController.
In line: let fromVC = transitionContext.viewController(forKey: .from) as? NavigationGalleryViewController should be nil, because it is not NavigationGalleryViewController but NavigationController.
If you want you can make smth like this: let fromVC = transitionContext.viewController(forKey: .from).childViewControllers.first as? NavigationGalleryViewController
I'm lost in the universe of the transitions. I want an interactive transition with a push segue. The following code works with a modal segue, but not with a push one :
(With a push segue, the animation is not interactive and is reversed)
FirstViewController.swift
let transitionManager = TransitionManager()
override func viewDidLoad() {
super.viewDidLoad()
transitionManager.sourceViewController = self
// Do any additional setup after loading the view.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let dest = segue.destinationViewController as UIViewController
dest.transitioningDelegate = transitionManager
transitionManager.destViewController = dest
}
TransitionManager.swift
class TransitionManager: UIPercentDrivenInteractiveTransition,UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate,UIViewControllerInteractiveTransitioning {
var interactive = false
var presenting = false
var panGesture : UIPanGestureRecognizer!
var destViewController : UIViewController!
var sourceViewController : UIViewController! {
didSet {
panGesture = UIPanGestureRecognizer(target: self, action: "gestureHandler:")
sourceViewController.view.addGestureRecognizer(panGesture)
}
}
func gestureHandler(pan : UIPanGestureRecognizer) {
let translation = pan.translationInView(pan.view!)
let velocity = pan.velocityInView(pan.view!)
let d = translation.x / pan.view!.bounds.width * 0.5
switch pan.state {
case UIGestureRecognizerState.Began :
interactive = true
sourceViewController.performSegueWithIdentifier("1to2", sender: self)
case UIGestureRecognizerState.Changed :
self.updateInteractiveTransition(d)
default :
interactive = false
if d > 0.2 || velocity.x > 0 {
self.finishInteractiveTransition()
}
else {
self.cancelInteractiveTransition()
}
}
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// create a tuple of our screens
let screens : (from:UIViewController, to:UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)
let container = transitionContext.containerView()
let toView = screens.to.view
let fromView = screens.from.view
toView.frame = CGRectMake(-320, 0, container.frame.size.width, container.frame.size.height)
container.addSubview(toView)
container.addSubview(fromView)
let duration = self.transitionDuration(transitionContext)
// perform the animation!
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
toView.frame.origin = container.frame.origin
fromView.frame.origin = CGPointMake(320, 0)
}, completion: { finished in
if(transitionContext.transitionWasCancelled()){
transitionContext.completeTransition(false)
UIApplication.sharedApplication().keyWindow.addSubview(screens.from.view)
}
else {
transitionContext.completeTransition(true)
UIApplication.sharedApplication().keyWindow.addSubview(screens.to.view)
}
})
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 1
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = true
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
}
Storyboard
The segue is from the FirstViewController to the SecondViewController.
Identifier : "1to2"
Segue : Push
Destination : Current
Thanks for your help