有没有办法为 .fullscreen 模态呈现的视图控制器进行交互式模态解雇?
Is there a way to have interactive modal dismissal for a .fullscreen modally presented view controller?
我想启用交互式模式关闭,它会随着用户手指在全屏模式呈现的视图控制器上平移 .fullscreen
。
我已经看到在内置它的 .pageSheet
和 .formSheet
上这样做是相当微不足道的,但还没有看到全屏的清晰示例。
我猜我需要在我的 vc 代码主体中添加一个平移手势,然后自己调整状态,但想知道是否有人知道到底需要做什么/是否有更简单的方法这样做的方法,因为 .fullscreen
案例
似乎要复杂得多
可以通过创建自定义 UIPresentationController
和 UIViewControllerTransitioningDelegate
来完成。假设我们有 TestViewController
,我们想要呈现 SecondViewController
,总 presentedHeight
为 1.0(全屏)。演示文稿将通过 @IBAction func buttonPressed
触发,并且可以通过向下拖动控制器来关闭(我们已经习惯了)。添加一些 backgroundEffect
以在用户向下滑动 SecondViewController
时逐渐改变(尤其是仅使用 0.6 的 presentedHeight
时)也很好。
首先我们定义了 OverlayViewController
,它将成为稍后呈现的 SecondViewController
的超类,并将包含 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)
}
接下来我们定义自定义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
}
创建 SecondViewController
子类 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)
}
添加 showOverlay()
将在 buttonPressed
之后触发的功能,并使您的呈现 UIViewController
(TestViewController) 符合 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
}
}
现在我们应该能够使用 showOverlay()
函数将 presentedHeight
设置为 1.0
和 .dim
背景效果来呈现 SecondViewController
.我们可以忽略 SecondViewController
类似于另一个模态演示。
我想启用交互式模式关闭,它会随着用户手指在全屏模式呈现的视图控制器上平移 .fullscreen
。
我已经看到在内置它的 .pageSheet
和 .formSheet
上这样做是相当微不足道的,但还没有看到全屏的清晰示例。
我猜我需要在我的 vc 代码主体中添加一个平移手势,然后自己调整状态,但想知道是否有人知道到底需要做什么/是否有更简单的方法这样做的方法,因为 .fullscreen
案例
可以通过创建自定义 UIPresentationController
和 UIViewControllerTransitioningDelegate
来完成。假设我们有 TestViewController
,我们想要呈现 SecondViewController
,总 presentedHeight
为 1.0(全屏)。演示文稿将通过 @IBAction func buttonPressed
触发,并且可以通过向下拖动控制器来关闭(我们已经习惯了)。添加一些 backgroundEffect
以在用户向下滑动 SecondViewController
时逐渐改变(尤其是仅使用 0.6 的 presentedHeight
时)也很好。
首先我们定义了
OverlayViewController
,它将成为稍后呈现的SecondViewController
的超类,并将包含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) }
接下来我们定义自定义
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 }
创建
SecondViewController
子类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) }
添加
showOverlay()
将在buttonPressed
之后触发的功能,并使您的呈现UIViewController
(TestViewController) 符合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 } }
现在我们应该能够使用
showOverlay()
函数将presentedHeight
设置为1.0
和.dim
背景效果来呈现SecondViewController
.我们可以忽略SecondViewController
类似于另一个模态演示。