I want to build a custom UIPresenterController that present the subsequent UIViewController from the right side of the screen to the left. I have built out the following UIPresenterController; however, on present, this comes from the bottom of the screen (while the dismiss works perfect). Does anyone have any suggestions on how I could fix this?
@available(iOS 13.0, *)
open class CUICRightSidePopUpTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
var presentedController: CUICRightSidePopUpPresentationController? = nil
public override init() {
super.init()
}
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
presentedController = CUICRightSidePopUpPresentationController(presentedViewController: presented, presenting: presenting)
guard let presentedController = presentedController else { return nil }
return presentedController
}
}
@available(iOS 13.0, *)
open class CUICRightSidePopUpPresentationController: UIPresentationController {
open override var frameOfPresentedViewInContainerView: CGRect {
let bounds = presentingViewController.view.bounds
let size = CGSize(width: UIScreen.main.bounds.width * viewRatio, height: UIScreen.main.bounds.height)
let origin = CGPoint(x: UIScreen.main.bounds.width - size.width, y: bounds.midY - size.height / 2)
return CGRect(origin: origin, size: size)
}
public override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
presentedViewController.view.addGestureRecognizer(panGestureRecognizer)
presentedView?.autoresizingMask = [
.flexibleTopMargin,
.flexibleBottomMargin,
.flexibleLeftMargin,
.flexibleRightMargin
]
presentedViewController.view.layer.cornerRadius = viewRadius
presentedViewController.view.clipsToBounds = true
dimmingView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
presentedView?.translatesAutoresizingMaskIntoConstraints = true
}
let viewRadius: CGFloat = 35
let viewRatio: CGFloat = 4 / 5
private let dimmingView: UIView = {
let dimmingView = UIView(frame: .zero)
dimmingView.translatesAutoresizingMaskIntoConstraints = false
return dimmingView
}()
@objc func dimmingViewTapped(_ sender: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true)
}
private lazy var panGestureRecognizer: UIPanGestureRecognizer = {
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction(sender:)))
return recognizer
}()
private let indicatorView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
view.clipsToBounds = true
return view
}()
open override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
containerView.addSubview(dimmingView)
containerView.addSubview(indicatorView)
containerView.addSubview(presentedViewController.view)
let initialFrame = CGRect(x: containerView.bounds.width, y: 0, width: frameOfPresentedViewInContainerView.width, height: frameOfPresentedViewInContainerView.height)
presentedViewController.view.frame = initialFrame
NSLayoutConstraint.activate([
dimmingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
dimmingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
dimmingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
dimmingView.topAnchor.constraint(equalTo: containerView.topAnchor),
])
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped(_:)))
dimmingView.addGestureRecognizer(tapGesture)
NSLayoutConstraint.activate([
indicatorView.widthAnchor.constraint(equalToConstant: 8),
indicatorView.heightAnchor.constraint(equalToConstant: 65),
indicatorView.trailingAnchor.constraint(equalTo: presentedViewController.view.leadingAnchor, constant: .smallPaddingRight),
indicatorView.centerYAnchor.constraint(equalTo: presentedViewController.view.centerYAnchor)
])
dimmingView.alpha = 0
presentedViewController.view.frame = CGRect(x: self.containerView?.bounds.width ?? 0, y: 0, width: self.frameOfPresentedViewInContainerView.width, height: self.frameOfPresentedViewInContainerView.height)
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
self?.dimmingView.alpha = 0.7
self?.presentedViewController.view.frame = self?.frameOfPresentedViewInContainerView ?? initialFrame
}, completion: nil)
}
open override func dismissalTransitionWillBegin() {
presentingViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
guard let self = self else { return }
self.dimmingView.alpha = 0
self.presentedViewController.view.frame = CGRect(x: self.containerView?.bounds.width ?? 0, y: 0, width: self.frameOfPresentedViewInContainerView.width, height: self.frameOfPresentedViewInContainerView.height)
self.indicatorView.frame.origin.x = (self.containerView?.bounds.width ?? 0)
}, completion: { [weak self] _ in
self?.dimmingView.removeFromSuperview()
self?.indicatorView.removeFromSuperview()
})
}
@objc func panGestureRecognizerAction(sender: UIPanGestureRecognizer) {
guard let containerView = containerView else { return }
let translation = sender.translation(in: containerView)
// Not allowing the user to drag the view upward
guard translation.x >= 0 else { return }
let initialFrame = frameOfPresentedViewInContainerView
// Move the main view and the indicator view horizontally
let newX = initialFrame.origin.x + translation.x
presentedViewController.view.frame.origin.x = newX
indicatorView.frame.origin.x = newX - CGFloat.smallPaddingLeft - indicatorView.frame.width
if sender.state == .ended {
let draggedToDismiss = (translation.x > containerView.bounds.width / 3.0)
let dragVelocity = sender.velocity(in: containerView)
if (dragVelocity.x >= 1300) || draggedToDismiss {
presentedViewController.dismiss(animated: true, completion: nil)
} else {
// Set back to original position of the view controller
UIView.animate(withDuration: 0.3) {
self.presentedViewController.view.frame.origin.x = initialFrame.origin.x
self.indicatorView.frame.origin.x = initialFrame.origin.x - 12 - self.indicatorView.frame.width
}
}
}
}
}
For additional context, this is how I am configuring transition in my view controller where configure()
is run in the viewDidLoad()
and the customTransitionDelegate is set to be CUICRightSidePopUpTransitioningDelegate
:
extension MyViewController {
func configure() {
modalPresentationStyle = .custom
transitioningDelegate = customTransitioningDelegate
}
}