Swift之模态弹窗自定义一 2024-09-22 周日
简介
iOS系统提供的模态弹窗已经足够好用了,所以这方面一直不用操心。
另外,自定义弹窗的实现方式过于复杂,很不好学,所以一直以来都不想学。
只是,现在自定义弹窗的需求越来越多,又不得不学一下。
测试VC
就一个背景为红色,最简单了。
class TempViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
/// 红色背景
view.backgroundColor = .red
}
}
调用的地方也采用最简单的方式:
let tempVc = TempViewController()
present(tempVc, animated: true) {
print("tempVc present 完成")
}
系统的弹出方式
说实话,系统的弹出方式已经足够好了,从下到上弹出来,调用的VC有个往后缩的动画,最后头部留点空间,手势向下可以让弹窗消失。
系统弹窗使用Lookin看视图结构,模态弹窗和导航栏push出来的是重叠的两套体系
视图层次结构transitioningDelegate
- 过渡动画,以代理的形式,留出了自定义的空间。这个代理是UIViewController的一个weak属性,思路和表格代理差不多。
extension UIViewController {
@available(iOS 7.0, *)
weak open var transitioningDelegate: UIViewControllerTransitioningDelegate?
}
- 代理的内容
@MainActor public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol {
@available(iOS 2.0, *)
optional func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
@available(iOS 2.0, *)
optional func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
optional func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
optional func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
@available(iOS 8.0, *)
optional func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?
}
-
从代理方法来看,这里有引入了3个新的角色。从命名推测
UIPresentationController:与模态对话框过渡有关,比如系统的,顶部留点空间,下拉手势消除对话框
UIViewControllerAnimatedTransitioning:跟动画有关,比如系统的从下往上进场
UIViewControllerInteractiveTransitioning:大概是动画过程中的自定义功能 -
Swift目前在推协议,在推代理的实现方式。但是从表格,到这个过渡动画,都可以看出,代理的学习和使用成本非常高,比Block、通知等形式难用多了。
-
虽然和表格一样,都是代理的实现方式,但是和表格的使用需求完全相反。表格要灵活,要适应各种场景;而过渡动画,要么用系统,要么自定义一套,大家共用就好。所以,这里计划用一个单例来做代理。这样就表明了最简单的意图:
(1)UIViewController的transitioningDelegate为nil,就用系统的过渡动画;
(2)UIViewController的transitioningDelegate被设置为自定义的类,就是自定义的过渡动画;
自定义过渡动画
-
创建一个基于NSObject的类TempTransitionDelegate作为过场动画的代理,提供默认单例default,表示共用自定义的额过场动画。
-
自定义UIPresentationController,替换系统的。默认什么也不做
import UIKit
class TempPresentation: UIPresentationController {
}
- TempTransitionDelegate实现代理UIViewControllerTransitioningDelegate;默认也是什么也不做,只是打印一下log
class TempTransitionDelegate: NSObject {
/// 默认单例
public static let `default`: TempTransitionDelegate = {
print("TempTransitionDelegate `default` 实例被创建")
return TempTransitionDelegate()
}()
}
/// 代理方法
extension TempTransitionDelegate: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("TempTransitionDelegate `animationController` forPresented 被调用")
return nil
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("TempTransitionDelegate `animationController` forDismissed 被调用")
return nil
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
print("TempTransitionDelegate `interactionControllerForPresentation` 被调用")
return nil
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
print("TempTransitionDelegate `interactionControllerForDismissal` 被调用")
return nil
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
presented.modalPresentationStyle = .custom
print("TempTransitionDelegate `presentationController` 被调用")
return TempPresentation(presentedViewController: presented, presenting: presenting)
}
}
- 在需要自定义的UIViewController中设置自定义转场动画代理。这里要注意的是需要在构造函数中设置才有效,在ViewDidLoad中设置已经迟了,不起效果。
另外,modalPresentationStyle = .custom属性需要设置成自定义,不然的话有可能还是系统的。
class TempViewController: UIViewController {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
/// 自定义弹窗方式
modalPresentationStyle = .custom
transitioningDelegate = TempTransitionDelegate.default
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
/// 红色背景
view.backgroundColor = .red
}
}
- 设置后的效果如下:没有动画,没有头部的缝隙,下拉手势也不能消除对话框。用Lookin查看,视图层次也简洁多了。
实现Sheet效果
(1)背景色就50%黑色,实现渐隐渐显效果,过场动画保持从底部弹窗的方式不变。
(2)点击背景还能消除对话框。
(3)红色的弹出视图,高度只要500pt就可以的,一半多点,只是做一些简单的交互操作。
背景视图
- UIPresentationController有个比较特殊的视图containerView,可以简单地认为就是Lookin中看到UITransitionView。这个view有可能为空,在UIPresentationController的构造函数期间是nil,但是在presentationTransitionWillBegin方法中已经稳定
// The view in which a presentation occurs. It is an ancestor of both the presenting and presented view controller's views.
// This view is being passed to the animation controller.
open var containerView: UIView? { get }
-
直接设置containerView的背景色,在这上面添加手势,也是可以的。不过,系统会把弹出控制器的view加到这个containerView上,containerView的alapha属性会影响子视图的显示效果,容易出现意料之外的情况。所以,这里,额外增加了一个和containerView同样大小的UIView(alphaCover)来做逐渐显示的动画,来做手势的载体。目的就是为了减少副作用。(系统提供的containerView,谁知道做了什么事)
-
设置弹出视图的高度:一般的UIViewController的view都是全屏的,这里可以设置大小,其中的frameOfPresentedViewInContainerView就是做这个事的
class TempPresentation: UIPresentationController {
/// containerView在这个时候已经存在,所以在这里加入自定义的view和oViewDidLoad有点像
/// 逐渐显现的动画做在自定义的
override func presentationTransitionWillBegin() {
containerView?.insertSubview(alphaCover, at: 0)
alphaCover.alpha = 0
UIView.animate(withDuration: 3) {
self.alphaCover.alpha = 1
}
}
/// 逐渐消失的动画
override func dismissalTransitionWillBegin() {
alphaCover.alpha = 1
UIView.animate(withDuration: 3) {
self.alphaCover.alpha = 0
}
}
/// 退出动画完成,去掉添加的辅助视图
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed {
alphaCover.removeFromSuperview()
}
}
/// 设置弹窗视图的高度
public var sheetHeight: CGFloat = 500
let phoneWidth = UIScreen.main.bounds.width
let phoneHeight = UIScreen.main.bounds.height
override var frameOfPresentedViewInContainerView: CGRect {
let frame = CGRect(origin: CGPoint(x: 0, y: (phoneHeight - sheetHeight)), size: CGSize(width: phoneWidth, height: sheetHeight))
return frame
}
/// 背景板,50%黑,退出手势
lazy var alphaCover: UIView = {
let cover = UIView()
cover.backgroundColor = .black.withAlphaComponent(0.5)
if let containerView = containerView, containerView.bounds.width > 0 {
cover.frame = containerView.bounds
} else {
cover.frame = CGRect(x: 0, y: 0, width: phoneWidth, height: phoneWidth)
}
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(coverTapAction))
cover.addGestureRecognizer(tapGesture)
return cover
}()
}
/// actions
extension TempPresentation {
@objc func coverTapAction() {
presentedViewController.dismiss(animated: true)
}
}
-
显示的时候,3秒逐渐显示的动画能完成。但是消失时,3秒的逐渐隐藏的动画显示不完全,不到1秒就消失了。这是因为动画过程没有定义,还是用了系统的,整个过程不到1秒。过场动画完成,整个containerView都会被系统收回,重新成为nil,那么所有的子视图当然会消失不见。
-
现在用