简介
UIPopoverPresentationController是用 iOS 8.0 后UIKit用来管理弹窗的对象,这个对象不用直接创建,UIKit 会自动创建此类的实例,可以通过配置UIViewController的popoverPresentationController 属性,来实现各种自定义的弹窗行为弹出窗口行为。
使用到的类和属性介绍
open class UIPopoverPresentationController : UIPresentationController {
//**************************************************************
/*
通过UIPopoverPresentationControllerDelegate 协议的方法可以自定义弹窗的行为。
*/
weak open var delegate: UIPopoverPresentationControllerDelegate?
//**************************************************************
open var permittedArrowDirections: UIPopoverArrowDirection
//************************通过下面三个属性确定锚点的位置*************************
/*
将此属性与 sourceRect 属性结合使用来指定弹窗的锚点位置。 或者,也可以使用 barButtonItem 属性指定弹出框的锚点位置。
*/
open var sourceView: UIView?
/*弹出框应该指向的sourceView坐标空间中的矩形。 如果设置了 barButtonItem,则忽略此属性。
从 iOS 13.2 开始,CGRectNull 的值会导致 popover 指向 sourceView 的当前帧,并在 sourceView 的大小发生变化时自动更新。 在 iOS 13.2 之前,不支 持空矩形。 iOS 13.2 中的默认值为 CGRectNull。 在 iOS 13.2 之前,默认值为 CGRectZero。这就意味在iOS13.2之前想正确的显示锚点的位置,必须设置sourceRect,在iOS13.2之后,不用设置sourceRect,只需设置sourceView也能显示在正确的位置
*/
open var sourceRect: CGRect
/*
设置点击UIBarButtonItem 弹出窗口,弹出框的箭头指向设置点击UIBarButtonItem。 或者,您可以使用 sourceView 和 sourceRect 属性指定弹出框的锚点位置。此属性的默认值为 nil。
*/
open var barButtonItem: UIBarButtonItem?
//*************************************************
//*****************************修改弹窗样式*********************************
/*
将此属性设置为 true 允许弹出框在空间受限时与 sourceRect 属性中的矩形重叠。 此属性的默认值为 false,可防止弹出框与源矩形重叠。
这个属性在弹窗中有键盘的时候非常有用,弹窗在键盘弹出的时候,如果被键盘遮住,如果这个属性为false,则会缩小弹出的高度,弹窗中UI会变形,如果设置为true,
则弹窗会上移,而不会改变弹窗的高度
*/
@available(iOS 9.0, *)
open var canOverlapSourceViewRect: Bool
/* 用户可以在弹出窗口可见时与之交互的一组视图。
当弹出框处于活动状态时,与其他视图的交互通常会被禁用,直到弹出框被解除。
将一组 UIView 对象分配给此属性会导致 UIKit 继续将触摸事件分派到您指定的视图。
passthroughViews的默认值是nil, 如果想和弹窗之外的某个view进行交会,可以将它添加进passthroughViews,这样交互的过程中弹窗也不会消失
*/
open var passthroughViews: [UIView]?
/*
设置弹出框背景颜色。 设置为 nil 以使用默认背景颜色。
这个值是修改 _UIVisualEffectContentView的颜色,而不是修改当UIPopoverPresentationController所在的viewController.view的背景色,
当UIPopoverPresentationController所在的viewController.view没有设置backgroundColor的时候能显示出来,
若viewController.view设置了backgroundColor就会被遮住,显示设置的背景色
*/
@NSCopying open var backgroundColor: UIColor?
/*
控制弹窗相对于屏幕的边距 默认边插入是沿每条边 10 个点。 在确定显示弹出框的位置时,弹窗会自动从区域中减去状态栏,因此无需将状态栏高度计入插图。
*/
open var popoverLayoutMargins: UIEdgeInsets
/*
修改弹窗的样式,如阴影,箭头高度等 默认是nil
*/
open var popoverBackgroundViewClass: UIPopoverBackgroundViewMethods.Type?
//**************************************************************
/*
在显示弹出窗口之前,将此属性设置为允许弹出窗口的箭头方向。 弹出窗口使用的实际箭头方向存储在 arrowDirection 属性中。
此属性的默认值为 any。
*/
open var arrowDirection: UIPopoverArrowDirection { get }
}
public protocol UIPopoverPresentationControllerDelegate : UIAdaptivePresentationControllerDelegate {
/* 即将呈现弹出窗口。
使用此方法可以对弹出窗口的外观和行为进行最后次自定义。
在调用此方法时,弹出窗口尚未出现在屏幕上。 您可以使用此方法修改 popover 呈现控制器的配置或执行您的应用程序所需的任何其他操作。
*/
@available(iOS 8.0, *)
optional func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController)
/*
是否应该解除弹出框。(iOS 8.0 - iOS 13.0)
*/
@available(iOS, introduced: 8.0, deprecated: 13.0)
optional func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool
//弹窗已经Dismiss
@available(iOS, introduced: 8.0, deprecated: 13.0)
optional func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController)
/*需要重新定位弹出框的位置。
弹出框表示控制器调用此方法以响应需要新的弹出框大小的界面更改。
例如,当必须调整弹出框的大小以为键盘腾出空间时,UIKit 会调用此方法。 您可以使用此方法获取弹出框的新大小,并可选择更改建议的视图和矩形。
如果你没有在你的委托中实现这个方法,UIKit 会将弹出框的大小调整为指定的矩形并将它(根据需要)移动到指定的视图。
popoverPresentationController:管理弹出框界面的弹出框展示控制器。
rect:在输入时,弹出框的新矩形。 这个 popover 在 view 参数中的视图坐标空间中。 如果要为弹出框建议不同的矩形,请将新值放入此参数中。
view: 在输入时,包含弹出框的新视图。 如果你想为 popover 建议一个不同的视图,把那个视图放在这个参数中
*/
@available(iOS 8.0, *)
optional func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>)
}
//这里插入一个知识点:从A控制器通过present的方式跳转到了B控制器,那么 A.presentedViewController 就是B控制器;B.presentingViewController 就是A控制器
public protocol UIAdaptivePresentationControllerDelegate : NSObjectProtocol {
/*
只支持 UIModalPresentationFullScreen 和 UIModalPresentationOverFullScreen
在iOS 8.3 及更高版本中,使用adaptivePresentationStyle(for:traitCollection:) 方法而不是此方法来处理所有特征更改。 如果您不实现该方法,则可以使用该方法在转换到水平紧凑环境时更改演示样式。
如果你没有实现这个方法或者如果你返回一个无效的样式,当前的呈现控制器返回它的首选默认样式。
*/
@available(iOS 8.0, *)
optional func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle
/* 返回 UIModalPresentationNone 将表明适配不应该发生。
当当前环境的特征即将改变时,表示控制器调用此方法。 此方法的实现可以返回用于指定特征的首选呈现样式。
如果您不返回允许的样式之一,则表示控制器将使用其首选的默认样式。如果你没有在你的委托中实现这个方法,UIKit 会调用adaptivePresentationStyle(for:) 方法。 如果想present为弹窗样式必须设置为 UIModalPresentationNone
*/
@available(iOS 8.3, *)
optional func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
/*
当size class更改导致底层呈现样式发生更改时,呈现控制器调用此方法以请求视图控制器以该新样式显示。 这种方法是您将当前视图控制器替换为更适合新呈现样式的视图控制器的机会。 例如,您可以使用此方法将导航控制器插入到您的视图层次结构中,以便在紧凑的环境中更轻松地推送新的视图控制器。 在这种情况下,您将返回一个导航控制器,其根视图控制器是当前呈现的视图控制器。 如果您愿意,您也可以返回一个完全不同的视图控制器。
如果你没有实现这个方法或者你的实现返回 nil,则呈现控制器使用它现有的presentedViewController。
*/
@available(iOS 8.0, *)
optional func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController?
/*
如果适配没有发生并使用了original style,则 UIModalPresentationNone 将作为参数传递。
当 size class 发生变化时,UIKit 会调用此方法来让您知道presentation controller将如何适应。 使用此方法进行任何其他更改。
例如,您可以使用过渡协调器对象为过渡创建附加动画。
*/
@available(iOS 8.3, *)
optional func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?)
/*
是否可以Dismiss
如果是使用 presentedViewController isModalInPresentation 为true的时候(isModalInPresentation = true 则不允许直接下拉dismiss,这是iOS 13后,present VC后的默认样式) 和调用 dismissf方法,这个方法是不会调用的
返回NO防止dismiss VC
系统可以随时调用该方法。 不保证此方法后会调用presentationControllerWillDismiss(_:) 或presentationControllerDidDismiss(_:)。 确保您对该方法的实现快速返回。
*/
@available(iOS 13.0, *)
optional func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool
/*
您可以使用此方法通过presentationController 的transitionCoordinator 设置动画或交互通知。
调用 dismiss方法,这个方法是不会调用的
*/
@available(iOS 13.0, *)
optional func presentationControllerWillDismiss(_ presentationController: UIPresentationController)
/*
vc dismiss 了
调用 dismissf方法,这个方法是不会调用的
*/
@available(iOS 13.0, *)
optional func presentationControllerDidDismiss(_ presentationController: UIPresentationController)
/*
UIKit 支持在presentationController.isModalInPresentation 返回true 或presentationControllerShouldDismiss(_:) 返回false 时拒绝关闭演示。
可以使用此方法通知用户为什么不能关闭VC
*/
@available(iOS 13.0, *)
optional func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController)
}
// 下面定义的方法都是抽象的; 为了继承`UIPopoverBackgroundView`,你必须提供下面每个方法的实现。 对于 `readwrite` 属性,您必须提供两个访问器的实现。
public protocol UIPopoverBackgroundViewMethods {
//箭头三角形底边的长度。
static func arrowBase() -> CGFloat
//描述背景视图的每个边缘与其内容视图的对应边缘之间的距离
static func contentViewInsets() -> UIEdgeInsets
//箭头三角形的高度
static func arrowHeight() -> CGFloat
}
@available(iOS 5.0, *)
open class UIPopoverBackgroundView : UIView, UIPopoverBackgroundViewMethods {
/*
箭头偏移表示箭头中心应该出现在离视图中心多远的地方。 对于 `UIPopoverArrowDirectionUp` 和 `UIPopoverArrowDirectionDown`,这是一个从左到右的偏移量; 箭头在中心的左边,arrowOffset为负数,箭头在中心的右边为正。 对于 `UIPopoverArrowDirectionLeft` 和 `UIPopoverArrowDirectionRight`,这是一个从上到下的偏移量; 箭头在中心的上边,arrowOffset为负数,箭头在中心的下边为正。。
*/
open var arrowOffset: CGFloat
/* `arrowDirection` 管理弹出框箭头指向的方向。 当弹出窗口在屏幕上仍然可见时,您可能需要更改箭头的方向。
*/
open var arrowDirection: UIPopoverArrowDirection
/* 此方法可能会被覆盖以防止在弹出框内绘制 contentinset 和阴影。 此方法的默认实现返回 YES。
*/
@available(iOS, introduced: 6.0, deprecated: 13.0, message: "No longer supported")
open class var wantsDefaultContentAppearance: Bool { get }
}
例子
-
通过设置sourceView弹出
class SourceViewVC: UIViewController { override func viewDidLoad() { super.viewDidLoad() } @IBAction func clickPopBtn(_ sender: UIButton) { let popVC = PopVC.init() //设置为弹窗样式 popVC.modalPresentationStyle = .popover //设置弹出大小 popVC.preferredContentSize = CGSize.init(width: 200, height: 200) popVC.popoverPresentationController?.sourceView = sender //在iOS13.2之前必须设置sourceRect,否则无法显示正确位置 popVC.popoverPresentationController?.sourceRect = sender.bounds popVC.popoverPresentationController?.permittedArrowDirections = .up popVC.popoverPresentationController?.delegate = self present(popVC, animated: true, completion: nil) } } extension SourceViewVC : UIPopoverPresentationControllerDelegate{ //是否允许点击背景消失 public func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool { return true } //是否允许点击背景消失 @available(iOS 13.0, *) public func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { return true } //必须设置为none,否则没有弹窗效果 public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } //Dismiss public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) { } //Dismiss @available(iOS 13.0, *) func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { } }
-
通过UIBarButtonItem弹出
class BarItemViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } @IBAction func clickBarItem(_ sender: UIBarButtonItem) { let popVC = PopVC.init() //设置为弹窗样式 popVC.modalPresentationStyle = .popover //设置弹出大小 popVC.preferredContentSize = CGSize.init(width: 200, height: 200) popVC.popoverPresentationController?.barButtonItem = sender popVC.popoverPresentationController?.permittedArrowDirections = .up popVC.popoverPresentationController?.delegate = self present(popVC, animated: true, completion: nil) } } extension BarItemViewController : UIPopoverPresentationControllerDelegate{ //是否允许点击背景消失 public func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool { return true } //是否允许点击背景消失 @available(iOS 13.0, *) public func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { return true } //必须设置为none,否则没有弹窗效果 public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } //Dismiss public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) { } //Dismiss @available(iOS 13.0, *) func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { } }
-
自定义弹窗,我这里定义的是一个居中无箭头的弹窗 可以修改弹窗的阴影,边框,边框颜色,圆角等属性,同时针对键盘也做了处理,当弹窗中有输入框的时候,弹窗会自动上移
import UIKit open class BaseCenterPopVC<T:BasePopoverBgView>: UIViewController,UIPopoverPresentationControllerDelegate{ ///用于确定弹窗的位置 private var sourceRect:CGRect = .zero ///弹窗的大小 public var size:CGSize = .zero ///是否可以点击背景消失 public var enableDismiss = true ///点击背景消失后的回调 public var didDismiss:(() -> Void)? = nil public var bgView:UIView! ///弹窗边框宽度 public var popBorderWidth: CGFloat{ return view.superview?.layer.borderWidth ?? 0 } ///弹窗边框颜色 public var popBorderColor: CGColor?{ return view.superview?.layer.borderColor } ///弹窗的圆角 public var popCornerRadius: CGFloat{ return view.superview?.layer.cornerRadius ?? 0 } ///弹窗要设置圆角的角 @available(iOS 11.0, *) public var popMaskedCorners: CACornerMask{ return view.superview?.layer.maskedCorners ?? [] } public init(size:CGSize) { super.init(nibName: nil, bundle: nil) self.size = size initPopVC() } required public init?(coder: NSCoder) { super.init(coder: coder) initPopVC() } func initPopVC(){ //必须在弹窗弹出前设置modalPresentationStyle modalPresentationStyle = .popover popoverPresentationController?.permittedArrowDirections = .up popoverPresentationController?.delegate = self popoverPresentationController?.canOverlapSourceViewRect = true popoverPresentationController?.popoverBackgroundViewClass = T.self } open override func viewDidLoad() { super.viewDidLoad() //防止使用storyboard后没有设置contentSize //子类要再super.viewDidLoad()之前设置size if size.equalTo(.zero) { fatalError("must set vc's size") } let screenSize = UIScreen.main.bounds.size sourceRect = CGRect.init(x:screenSize.width/2, y: 0, width: 1, height:(screenSize.height - size.height)/2) preferredContentSize = size popoverPresentationController?.sourceRect = sourceRect //从A控制器通过present的方式跳转到了B控制器,那么 A.presentedViewController 就是B控制器;B.presentingViewController 就是A控制器 //不能在init中,因为presentingViewController在init中为nil weak var presentingView = presentingViewController?.view popoverPresentationController?.sourceView = presentingView bgView = UIView.init(frame: UIScreen.main.bounds) bgView.backgroundColor = UIColor.init(white: 0, alpha: 0.25) } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) presentingViewController?.view.addSubview(bgView) } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) bgView.removeFromSuperview() } open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() view.superview?.layer.borderWidth = popBorderWidth view.superview?.layer.borderColor = popBorderColor view.superview?.layer.cornerRadius = popCornerRadius if #available(iOS 11.0, *) { view.superview?.layer.maskedCorners = popMaskedCorners } } //MARK:UIPopoverPresentationControllerDelegate public func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool { return enableDismiss } @available(iOS 13.0, *) public func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { return enableDismiss } public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) { didDismiss?() didDismiss = nil } @available(iOS 13.0, *) public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { didDismiss?() didDismiss = nil } } //移除了箭头的弹窗背景 open class BasePopoverBgView : UIPopoverBackgroundView{ ///弹窗背景的阴影偏移 public var popShadowOffset: CGSize{ return layer.shadowOffset } ///弹窗背景的阴影圆角 public var popShadowRadius: CGFloat{ return layer.shadowRadius } ///弹窗背景的阴影不透明度 public var popShadowOpacity: Float{ return layer.shadowOpacity } ///弹窗背景的阴影颜色 public var popShadowColor: CGColor?{ return layer.shadowColor } public override static func contentViewInsets() -> UIEdgeInsets { return UIEdgeInsets.zero } public override static func arrowHeight() -> CGFloat { return 0 } open override var arrowDirection: UIPopoverArrowDirection { get { return UIPopoverArrowDirection.up } set {setNeedsLayout()} } open override var arrowOffset: CGFloat { get { return 0 } set {setNeedsLayout()} } open override func layoutSubviews() { super.layoutSubviews() self.layer.shadowOffset = popShadowOffset self.layer.shadowRadius = popShadowRadius self.layer.shadowOpacity = popShadowOpacity self.layer.shadowColor = popShadowColor } } open class PopDefaultConfig { static let shared = PopDefaultConfig() ///弹窗背景的阴影偏移 public var shadowOffset:CGSize = CGSize.init(width: 0, height: 4) ///弹窗背景的阴影圆角 public var shadowRadius:CGFloat = 4 ///弹窗背景的阴影不透明度 public var shadowOpacity:Float = 0.2 ///弹窗背景的阴影颜色 public var shadowColor:UIColor = .gray ///弹窗边框宽度 public var borderWidth: CGFloat = 0 ///弹窗边框颜色 public var borderColor: UIColor = .white ///弹窗的圆角 public var cornerRadius: CGFloat = 0 ///弹窗要设置圆角的角 public var maskedCorners:CACornerMask = [.layerMinXMinYCorner,.layerMaxXMinYCorner,.layerMinXMaxYCorner,.layerMaxXMaxYCorner] private init(){} } open class DefaultPopoverBgView: BasePopoverBgView { ///弹窗背景的阴影偏移 public override var popShadowOffset: CGSize{ return PopDefaultConfig.shared.shadowOffset } ///弹窗背景的阴影圆角 public override var popShadowRadius: CGFloat{ return PopDefaultConfig.shared.shadowRadius } ///弹窗背景的阴影不透明度 public override var popShadowOpacity: Float{ return PopDefaultConfig.shared.shadowOpacity } ///弹窗背景的阴影颜色 public override var popShadowColor: CGColor?{ return PopDefaultConfig.shared.shadowColor.cgColor } } open class DefaultPopoVC: BaseCenterPopVC<DefaultPopoverBgView> { ///弹窗边框宽度 public override var popBorderWidth: CGFloat{ return PopDefaultConfig.shared.borderWidth } ///弹窗边框颜色 public override var popBorderColor: CGColor?{ return PopDefaultConfig.shared.borderColor.cgColor } ///弹窗的圆角 public override var popCornerRadius: CGFloat{ return PopDefaultConfig.shared.cornerRadius } ///弹窗要设置圆角的角 @available(iOS 11.0, *) public override var popMaskedCorners: CACornerMask{ return PopDefaultConfig.shared.maskedCorners } }
class CustomVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//通过 PopDefaultConfig全局配置弹窗的属性
PopDefaultConfig.shared.borderWidth = 1
PopDefaultConfig.shared.borderColor = UIColor.blue
PopDefaultConfig.shared.cornerRadius = 10
}
@IBAction func clickGlobalPopBtn(_ sender: Any) {
let vc = GlobalPopVC.init(size: CGSize.init(width: 200, height: 200))
present(vc, animated: true, completion: nil)
}
@IBAction func clickCustomPopBtn(_ sender: Any) {
let vc = CustomPopVC.init(size: CGSize.init(width: 200, height: 200))
present(vc, animated: true, completion: nil)
}
//Storyboard中弹出CustomSBPopVC
}
class GlobalPopVC: DefaultPopoVC {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.orange
}
}
class CustomPopVC: BaseCenterPopVC<CustomBgView> {
///弹窗边框宽度
override var popBorderWidth: CGFloat{
return 2
}
///弹窗边框颜色
override var popBorderColor: CGColor?{
return UIColor.green.cgColor
}
///弹窗的圆角
override var popCornerRadius: CGFloat{
return 20
}
///弹窗要设置圆角的角
override var popMaskedCorners: CACornerMask{
return [.layerMinXMinYCorner,.layerMaxXMaxYCorner]
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.red
}
}
class CustomBgView: BasePopoverBgView {
///弹窗背景的阴影偏移
override var popShadowOffset: CGSize{
return CGSize.init(width: 10, height: 10)
}
///弹窗背景的阴影圆角
override var popShadowRadius: CGFloat{
return 10
}
///弹窗背景的阴影不透明度
override var popShadowOpacity: Float{
return 0.2
}
///弹窗背景的阴影颜色
override var popShadowColor: CGColor?{
return UIColor.red.cgColor
}
}
class CustomSBPopVC: DefaultPopoVC {
override func viewDidLoad() {
//必须在父类调用viewDidLoad之前设置size
size = CGSize.init(width: UIScreen.main.bounds.size.width, height: 300)
super.viewDidLoad()
}
}
extension CustomSBPopVC : UITextFieldDelegate{
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}