几天前我为此创建了一个开源snippet。它包括箭头和 passThroughViews,因此您可以在不关闭弹出框的情况下点击它们,但它们都不是必需的参数。
您可以使用以下方式从任何视图控制器调用它:
vc.showPopover(message: "Some message for a dynamically-sized popover with a left arrow.", sourceView: self.button, sourceRect: self.button.bounds, arrowDirection: .left, passthroughViews: [button])
在导航栏中右侧 UIBarButtonItem 所在的下方显示一个弹出框:
guard let navBarView = navigationController?.navigationBar.subviews.first else { return }
let topPadding = self.navigationController?.view.safeAreaInsets.top ?? 0
self.showPopover(message: "This will show up on your rightmost bar button item in your navigation.", sourceView: navBarView, sourceRect: CGRect.init(x: navBarView.frame.width - 40, y: navBarView.frame.height + topPadding, width: 0, height: 0), arrowDirection: .up, passthroughViews: (navigationController?.navigationBar.subviews)!)
您需要的第一部分是 PopoverViewController 类:
import Foundation
import UIKit
class PopoverViewController: UIViewController {
@IBOutlet weak var labelMessage: UILabel!
var message:String?
weak var sendingViewController: UIViewController?
var arrowDirection: UIPopoverArrowDirection = .any
let ltPadding: CGFloat = 20 // leading/trailing padding
let tbPadding: CGFloat = 14 // top/bottom padding
let arrowPadding: CGFloat = 13 // popover arrow padding
let dimLevel: CGFloat = 0.6
override func viewDidLoad() {
super.viewDidLoad()
if let message = self.message {
self.labelMessage.text = message
}
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
view.addGestureRecognizer(tap)
// dim sendingViewController
UIView.animate(withDuration: AntimationDuration.fadeInOut.rawValue, animations: {
self.sendingViewController?.view.alpha = self.dimLevel
})
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("arrowDirection: \(arrowDirection)")
let item: UIView = self.labelMessage
guard let superview = item.superview else { return }
// all constaints must be set for a proper display
// set centered constraincts
item.centerYAnchor.constraint(equalTo: superview.centerYAnchor).isActive = true
item.centerXAnchor.constraint(equalTo: superview.centerXAnchor).isActive = true
// iOS grows view if there is an arrow, so adjust anchors to fix offset
if arrowDirection.rawValue == 2 { //.down {
item.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: ltPadding).isActive = true
item.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: ltPadding).isActive = true
item.topAnchor.constraint(equalTo: superview.topAnchor, constant: tbPadding).isActive = true
item.bottomAnchor.constraint(greaterThanOrEqualTo: superview.bottomAnchor, constant: tbPadding + arrowPadding).isActive = true
item.widthAnchor.constraint(equalTo: superview.widthAnchor, constant: -(ltPadding * 2)).isActive = true
item.heightAnchor.constraint(equalTo: superview.heightAnchor, constant: -(tbPadding * 2) - arrowPadding).isActive = true
} else if arrowDirection.rawValue == 1 { //.up {
item.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: ltPadding).isActive = true
item.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: ltPadding).isActive = true
item.topAnchor.constraint(equalTo: superview.topAnchor, constant: tbPadding + arrowPadding).isActive = true
item.bottomAnchor.constraint(greaterThanOrEqualTo: superview.bottomAnchor, constant: tbPadding).isActive = true
item.widthAnchor.constraint(equalTo: superview.widthAnchor, constant: -(ltPadding * 2)).isActive = true
item.heightAnchor.constraint(equalTo: superview.heightAnchor, constant: -(tbPadding * 2) - arrowPadding).isActive = true
} else if arrowDirection.rawValue == 4 { //.left {
item.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: ltPadding + arrowPadding).isActive = true
item.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: ltPadding).isActive = true
item.topAnchor.constraint(equalTo: superview.topAnchor, constant: tbPadding).isActive = true
item.bottomAnchor.constraint(greaterThanOrEqualTo: superview.bottomAnchor, constant: tbPadding).isActive = true
item.widthAnchor.constraint(equalTo: superview.widthAnchor, constant: -(ltPadding * 2) - arrowPadding).isActive = true
item.heightAnchor.constraint(equalTo: superview.heightAnchor, constant: -(tbPadding * 2)).isActive = true
} else if arrowDirection.rawValue == 8 { //.right {
item.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: ltPadding).isActive = true
item.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: ltPadding + arrowPadding).isActive = true
item.topAnchor.constraint(equalTo: superview.topAnchor, constant: tbPadding).isActive = true
item.bottomAnchor.constraint(greaterThanOrEqualTo: superview.bottomAnchor, constant: tbPadding).isActive = true
item.widthAnchor.constraint(equalTo: superview.widthAnchor, constant: -(ltPadding * 2) - arrowPadding).isActive = true
item.heightAnchor.constraint(equalTo: superview.heightAnchor, constant: -(tbPadding * 2)).isActive = true
} else {
// center if no arrows
item.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: ltPadding).isActive = true
item.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: ltPadding).isActive = true
item.topAnchor.constraint(equalTo: superview.topAnchor, constant: tbPadding).isActive = true
item.bottomAnchor.constraint(greaterThanOrEqualTo: superview.bottomAnchor, constant: tbPadding).isActive = true
item.widthAnchor.constraint(equalTo: superview.widthAnchor, constant: -(ltPadding * 2)).isActive = true
item.heightAnchor.constraint(equalTo: superview.heightAnchor, constant: -(tbPadding * 2)).isActive = true
}
}
@objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
self.dismiss(animated: true, completion: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
UIViewController.existingPopover = nil
// restore dimmed sendingViewController
UIView.animate(withDuration: AntimationDuration.fadeInOut.rawValue, animations: {
self.sendingViewController?.view.alpha = 1
})
}
}
您还需要 AlwaysPresentAsPopover 类:
import Foundation
import UIKit
class AlwaysPresentAsPopover : NSObject, UIPopoverPresentationControllerDelegate {
// `sharedInstance` because the delegate property is weak - the delegate instance needs to be retained.
private static let sharedInstance = AlwaysPresentAsPopover()
private override init() {
super.init()
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
static func configurePresentation(forController controller : UIViewController) -> UIPopoverPresentationController {
controller.modalPresentationStyle = .popover
let presentationController = controller.presentationController as! UIPopoverPresentationController
presentationController.delegate = AlwaysPresentAsPopover.sharedInstance
return presentationController
}
}
以及扩展功能:
extension UIViewController {
static var existingPopover: PopoverViewController?
func showPopover(message: String, sourceView: UIView, sourceRect: CGRect, arrowDirection: UIPopoverArrowDirection = [], passthroughViews: [UIView]? = []) {
DispatchQueue.main.async {
// init view controller
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "popover") as! PopoverViewController
controller.message = message
controller.sendingViewController = self
controller.arrowDirection = arrowDirection
// set preferred size
let margins = CGPoint(x: 20, y: 14)
let textSize = message.size(width: 220)
let adjustedSize = CGSize(width: textSize.width + (margins.x * 2), height: textSize.height + (margins.y * 2))
controller.preferredContentSize = adjustedSize
// create view controller as popover style presentation instead of a normal view controller
let presentationController = AlwaysPresentAsPopover.configurePresentation(forController: controller)
presentationController.sourceView = sourceView
presentationController.sourceRect = sourceRect
presentationController.permittedArrowDirections = arrowDirection
// passthroughViews are views that can be tapped without dismissing the popover
if let views = passthroughViews {
presentationController.passthroughViews = views
}
// if there is an existing presenting view controller, dismiss it before presenting this popover
if let existing = UIViewController.existingPopover {
existing.dismiss(animated: true, completion: {
self.present(controller, animated: true, completion: {
UIViewController.existingPopover = controller
})
})
} else {
self.present(controller, animated: true, completion: {
UIViewController.existingPopover = controller
})
}
}
}
}
extension String {
func size(width:CGFloat = 220.0, font: UIFont = UIFont.systemFont(ofSize: 17.0, weight: .regular)) -> CGSize {
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = self
label.sizeToFit()
return CGSize(width: label.frame.width, height: label.frame.height)
let squared = square(5)
print(squared) // prints 25
}
func square(_ number: Int) -> Int {
return number * number
}
}