// // FSPopoverView.swift // FSPopoverView // // Created by Sheng on 2022/4/2. // Copyright © 2023 Sheng. All rights reserved. // import UIKit open class FSPopoverView: UIView { // MARK: ArrowDirection public enum ArrowDirection { case up, down, left, right } // MARK: Properties/Open /// The object that acts as the data source of the popover view. /// /// * The data source must adopt the FSPopoverViewDataSource protocol. /// * The data source is not retained. /// * A reload request will be set when this property is set. /// weak open var dataSource: FSPopoverViewDataSource? { didSet { setNeedsReload() } } /// The transitioning delegate object is a custom object that you provide /// and that conforms to the FSPopoverViewAnimatedTransitioning protocol. /// /// * You can use this property to custom the transition animation of popover view. /// * A default transitioning delegate is set for popover view. /// * This object is not retained. /// weak open var transitioningDelegate: FSPopoverViewAnimatedTransitioning? { didSet { if let scale = scaleTransition, scale !== transitioningDelegate { scaleTransition = nil } } } /// The direction of the popover's arrow. /// /// * You can change this property even though the popover view is displaying. /// * A reload request will be set when this property is changed. /// * See ``autosetsArrowDirection`` property for more information about this /// property. /// open var arrowDirection = FSPopoverView.ArrowDirection.up { didSet { if arrowDirection != oldValue, !autosetsArrowDirection, !isReloading { setNeedsReload() } } } /// Whether to set the arrow direction automatically. /// Defaults to true. /// /// * When this property is true, the popover view will determine the direction /// of the arrow and update the ``arrowDirection`` property automatically. /// * When this property is false, The popover view will calculate it's position /// according to the ``arrowDirection`` property, and the ``arrowDirection`` property /// will never be changed by inside. /// * A reload request will be set when this property is changed. /// /// - Important /// * When this property is true, the popover view will calculate the appropriate /// position based on the size of it's container and the position of the arrow. /// So, when you set this property to false, you should ensure that there is enough /// space in the container for the popover view to appear, otherwise the popover /// view may be in the wrong place. /// open var autosetsArrowDirection = true { didSet { if autosetsArrowDirection != oldValue { setNeedsReload() } } } // MARK: Properties/Public /// The location of the arrow's vertex. /// Defaults to (0, 0). /// /// * This point is in the coordinate system of `containerView`. /// * This point will be recalculated on reload operation. /// * The value of ``showsArrow`` has no effect on this property. /// final public private(set) var arrowPoint: CGPoint = .zero /// The container view displaying the popover view. /// This view will be created when the popover view needs to be displayed. /// /// If popover view is displaying in a specified view, the specified view will be the superview /// of the container view. Otherwise, a window will be created automatically inside the popover /// view as the superview of the container view, and this window will be the same size as the /// current screen. /// /// The popover view is added to the container view. /// The view hierarchy is: /// ``` /// specified view / window /// - container view (same size as specified view / window) /// - dim background view (same size as container view) /// - user interaction view (same size as container view) /// - popover view /// - popover container view /// - background view /// - content view (from data source) /// ``` /// final weak public private(set) var containerView: UIView? /// Arrow will be hidden when this property is set to false. /// Default value see `FSPopoverViewAppearance`. /// /// * A reload request will be set when this property is changed. /// final public var showsArrow: Bool { didSet { if showsArrow != oldValue { setNeedsReload() } } } /// Whether needs to show a dim background on container view. /// Default value see ``FSPopoverViewAppearance``. final public var showsDimBackground: Bool { didSet { dimBackgroundView.isHidden = !showsDimBackground } } /// The corner radius of the popover view. /// Default value see ``FSPopoverViewAppearance``. /// /// * A reload request will be set when this property is set. /// final public override var cornerRadius: CGFloat { didSet { if cornerRadius != oldValue { setNeedsReload() } } } /// The size of the arrow. /// Default value see ``FSPopoverViewAppearance``. /// /// * A reload request will be set when this property is set. /// final public var arrowSize: CGSize { didSet { if arrowSize != oldValue { setNeedsReload() } } } /// The width of the popover view border. /// Default value see ``FSPopoverViewAppearance``. /// /// * A reload request will be set when this property is changed. /// final public override var borderWidth: CGFloat { didSet { if borderWidth != oldValue { setNeedsReload() } } } /// The color of the popover view border. /// Default value see ``FSPopoverViewAppearance``. /// /// * A reload request will be set when this property is set. /// final public override var borderColor: UIColor? { didSet { setNeedsReload() } } /// The color of the popover view shadow. /// Default value see ``FSPopoverViewAppearance``. /// /// * A reload request will be set when this property is set. /// final public var shadowColor: UIColor? { didSet { setNeedsReload() } } /// The radius of the popover view shadow. /// Default value see ``FSPopoverViewAppearance``. /// /// * A reload request will be set when this property is changed. /// final public var shadowRadius: CGFloat { didSet { if shadowRadius != oldValue { setNeedsReload() } } } /// The opacity of the popover view shadow. /// The value in this property must be in the range 0.0 (transparent) to 1.0 (opaque). /// Default value see ``FSPopoverViewAppearance``. /// /// * A reload request will be set when this property is changed. /// final public var shadowOpacity: Float { didSet { if shadowOpacity != oldValue { setNeedsReload() } } } // MARK: Properties/Override /// It's objected to use this property to set the background color of popover view. /// Use ``backgroundView`` of ``dataSource`` instead. @available(*, unavailable) final public override var backgroundColor: UIColor? { get { return nil } set {} } // MARK: Properties/Private private var needsReload = false private var isFreezing = false private var isReloading = false private let delegateRouter = FSPopoverViewDelegateRouter() weak private var backgroundView: UIView? weak private var contentView: UIView? /// ``backgroundView`` and ``contentView`` will be added to this view. /// /// * This view will be the same size as the popover view. /// private lazy var popoverContainerView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() weak private var borderLayer: CALayer? weak private var shadowLayer: CALayer? /// Size of `containerView`. private var containerSize: CGSize = .zero /// This rect is in the coordinate system of ``containerView``. private var arrowReferRect: CGRect = .zero /// If there is no specified view to display popover view, /// this window will be created. private var displayWindow: UIWindow? /// The dim background on container view. private lazy var dimBackgroundView: UIView = { let view = UIView() view.isHidden = true view.backgroundColor = .init(white: 0.0, alpha: 0.25) view.isUserInteractionEnabled = false return view }() /// The view that receives user interaction. private lazy var userInteractionView: UIView = { let view = UIView() view.backgroundColor = .clear view.isUserInteractionEnabled = true do { let tap = UITapGestureRecognizer(target: self, action: #selector(p_handleTap)) tap.delegate = delegateRouter view.addGestureRecognizer(tap) } return view }() /// The default value of transitioning delegate. private var scaleTransition: FSPopoverViewTransitionScale? // MARK: Initialization public init() { // appearance let appearance = FSPopoverViewAppearance.shared showsArrow = appearance.showsArrow showsDimBackground = appearance.showsDimBackground // cornerRadius = appearance.cornerRadius arrowSize = appearance.arrowSize // borderWidth = appearance.borderWidth // borderColor = appearance.borderColor shadowColor = appearance.shadowColor shadowRadius = appearance.shadowRadius shadowOpacity = appearance.shadowOpacity super.init(frame: .zero) cornerRadius = appearance.cornerRadius borderWidth = appearance.borderWidth borderColor = appearance.borderColor p_didInitialize() } @available(*, unavailable) required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: Methods/Override override open func layoutSubviews() { super.layoutSubviews() reloadDataIfNeeded() } open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if #unavailable(iOS 17) { setNeedsReload() } } @available(*, unavailable) open override class func appearance() -> Self { return super.appearance() } @available(*, unavailable) open override class func appearance(for trait: UITraitCollection) -> Self { return super.appearance(for: trait) } @available(*, unavailable) open override class func appearance(whenContainedInInstancesOf containerTypes: [UIAppearanceContainer.Type]) -> Self { return super.appearance(whenContainedInInstancesOf: containerTypes) } @available(*, unavailable) open override class func appearance(for trait: UITraitCollection, whenContainedInInstancesOf containerTypes: [UIAppearanceContainer.Type]) -> Self { return super.appearance(for: trait, whenContainedInInstancesOf: containerTypes) } // MARK: Methods/Open /// Tells the popover view to reload all of its contents. /// /// - Requires /// * Subclasses **must** call `super.setNeedsReload()` when overriding this method, /// otherwise some bugs may occur. /// /// - Note /// * This method makes a note of the request and returns immediately. This method /// does not force an immediate reload, all of the contents will reload in view's /// next layout update cycle. This behavior allows you to consolidate all of your /// content reloads to one layout update cycle, which is usually better for performance. /// * You should call this method on the main thread. /// open func setNeedsReload() { p_mainThreadCheck() needsReload = true setNeedsLayout() } /// Reload the contents immediately if the reload operation is pending. /// /// - Requires /// * Subclasses **must** call `super.reloadDataIfNeeded()` when overriding this method, /// otherwise some bugs may occur. /// /// - Note /// * Use this method to force the popover view to reload its contents immediately, but if /// the reload operation is not pending, this method exits without modifying the contents /// or calling any content-related callbacks. /// * You should call this method on the main thread. /// open func reloadDataIfNeeded() { p_mainThreadCheck() if needsReload { reloadData() } } /// Reload the contents immediately, even without any reload requests. /// /// - Requires /// * Subclasses **must** call `super.reloadData()` when overriding this method, /// otherwise some bugs may occur. /// /// - Note /// * Calling this method will not have any animation, even if some contents are /// changed, like size of content and direction of arrow and so on. /// * You should call this method on the main thread. /// open func reloadData() { p_mainThreadCheck() isReloading = true p_reloadData() isReloading = false } /// Get the transition context for the specified scene. open func transitionContext(for scene: FSPopoverViewTransitionContext.Scene) -> FSPopoverViewTransitionContext { return FSPopoverViewTransitionContext(scene: scene, popoverView: self, dimBackgroundView: dimBackgroundView) } /// Presents the popover and anchors it to the specified view. open func present(fromView view: UIView, displayIn specifiedView: UIView? = nil, animated: Bool = true, completion: (() -> Void)? = nil) { p_present(from: view.frame, in: view.superview, displayIn: specifiedView, animated: animated, completion: completion) } /// Presents the popover and anchors it to the specified location. /// - Parameters: /// - view: The view containing the point. open func present(fromPoint point: CGPoint, in view: UIView? = nil, displayIn specifiedView: UIView? = nil, animated: Bool = true, completion: (() -> Void)? = nil) { p_present(from: .init(origin: point, size: .zero), in: view, displayIn: specifiedView, animated: animated, completion: completion) } /// Presents the popover and anchors it to the specified rect. /// - Parameters: /// - view: The view containing the rectangle. open func present(fromRect rect: CGRect, in view: UIView? = nil, displayIn specifiedView: UIView? = nil, animated: Bool = true, completion: (() -> Void)? = nil) { p_present(from: rect, in: view, displayIn: specifiedView, animated: animated, completion: completion) } /// Presents the popover and anchors it to the specified bar item. open func present(fromBarItem barItem: UIBarItem, animated: Bool = true, completion: (() -> Void)? = nil) { guard let view = barItem.value(forKey: "view") as? UIView else { #if DEBUG fatalError("The value of UIBarItem has been changed, this method can not be used anymore.") #else return #endif } p_present(from: view.frame, in: view.superview, animated: animated, completion: completion) } /// Dismiss popover view. open func dismiss(animated: Bool = true, isSelection: Bool = false, completion: (() -> Void)? = nil) { p_dismiss(animated: animated, isSelection: isSelection, completion: completion) } } // MARK: - Methods/Private private extension FSPopoverView { /// Invoked after initialization. func p_didInitialize() { popoverContainerView.backgroundColor = .clear delegateRouter.gestureRecognizerShouldBeginHandler = { [unowned self] gestureRecognizer in if let view = gestureRecognizer.view, view === self.userInteractionView { if self.isFreezing { return true } let location = gestureRecognizer.location(in: view) return !self.frame.contains(location) } return false } if #available(iOS 17, *) { registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, previousTraitCollection: UITraitCollection) in self.setNeedsReload() } } addSubview(popoverContainerView) popoverContainerView.inner.makeMarginConstraints(to: self) scaleTransition = FSPopoverViewTransitionScale() transitioningDelegate = scaleTransition } func p_mainThreadCheck() { #if DEBUG if !Thread.isMainThread { fatalError("You must call this method on the main thread.") } #endif } func p_getArrowSize() -> CGSize { guard showsArrow else { return .zero } var size = arrowSize size.width = max(size.width, 0) size.height = max(size.height, 0) return size } func p_createContainerView() -> UIView { let view = UIView() view.backgroundColor = .clear return view } func p_createDisplayWindow() -> UIWindow { let window = UIWindow() window.windowLevel = .statusBar - 1 window.backgroundColor = .clear if #available(iOS 13.0, *), let scene = UIApplication.shared.connectedScenes.filter({ $0.activationState == .foregroundActive }).first as? UIWindowScene { window.windowScene = scene } return window } func p_prepareForDisplaying() { // freezes the popover view before the popover view finishes displaying operation. isFreezing = true alpha = 1.0 transform = .identity removeFromSuperview() containerSize = .zero containerView?.removeFromSuperview() dimBackgroundView.alpha = 1.0 dimBackgroundView.transform = .identity p_destroyDisplayWindow() } func p_setContainerView(_ view: UIView) { containerView = view dimBackgroundView.removeFromSuperview() view.addSubview(dimBackgroundView) dimBackgroundView.inner.makeMarginConstraints(to: view) userInteractionView.removeFromSuperview() view.addSubview(userInteractionView) userInteractionView.inner.makeMarginConstraints(to: view) } func p_destroyDisplayWindow() { displayWindow?.isHidden = true displayWindow?.subviews.forEach { $0.removeFromSuperview() } displayWindow = nil } /// Reset all contents to default values. func p_resetContents() { borderLayer?.removeFromSuperlayer() shadowLayer?.removeFromSuperlayer() contentView?.removeFromSuperview() backgroundView?.removeFromSuperview() borderLayer = nil shadowLayer = nil contentView = nil backgroundView = nil } /// Reload all contents and recalculate the position and size of the popover view. func p_reloadData() { p_mainThreadCheck() needsReload = false // clear old contents p_resetContents() guard containerSize.width > 0, containerSize.height > 0 else { return } // container safe area insets let safeAreaInsets = dataSource?.containerSafeAreaInsets(for: self) ?? .zero let safeContainerRect = CGRect(origin: .zero, size: containerSize).inset(by: safeAreaInsets) // content size let realContentSize = dataSource?.contentSize(for: self) ?? .zero let contentSize: CGSize // arrow size let arrowSize = p_getArrowSize() // arrow direction do { let horizontalContentSize: CGSize = { var size = CGSize(width: cornerRadius * 2 + 10.0, height: arrowSize.height + cornerRadius * 2 + 10.0) if realContentSize.width > size.width { size.width = realContentSize.width } if realContentSize.height > size.height { size.height = realContentSize.height } return size }() let verticalContentSize: CGSize = { var size = CGSize(width: arrowSize.width + cornerRadius * 2 + 10.0, height: cornerRadius * 2 + 10.0) if realContentSize.width > size.width { size.width = realContentSize.width } if realContentSize.height > size.height { size.height = realContentSize.height } return size }() if autosetsArrowDirection { let referRect = arrowReferRect.insetBy(dx: -arrowSize.height, dy: -arrowSize.height) let topSpace = CGSize(width: safeContainerRect.width, height: max(0.0, referRect.minY - safeContainerRect.minY)) let leftSpace = CGSize(width: max(0.0, referRect.minX - safeContainerRect.minX), height: safeContainerRect.height) let bottomSpace = CGSize(width: safeContainerRect.width, height: max(0.0, safeContainerRect.maxY - referRect.maxY)) let rightSpace = CGSize(width: max(0.0, safeContainerRect.maxX - referRect.maxX), height: safeContainerRect.height) // priority: up > down > left > right if bottomSpace.width >= verticalContentSize.width && bottomSpace.height >= verticalContentSize.height { contentSize = verticalContentSize arrowDirection = .up } else if topSpace.width >= verticalContentSize.width && topSpace.height >= verticalContentSize.height { contentSize = verticalContentSize arrowDirection = .down } else if rightSpace.width >= horizontalContentSize.width && rightSpace.height >= horizontalContentSize.height { contentSize = horizontalContentSize arrowDirection = .left } else if leftSpace.width >= horizontalContentSize.width && leftSpace.height >= horizontalContentSize.height { contentSize = horizontalContentSize arrowDirection = .right } else { // `up` will be set if there is not enough space to show popover view. arrowDirection = .up contentSize = verticalContentSize #if DEBUG let message = """ ⚠️ Not enough space to show popover view, \ you may need to check if the popover view is showing on the wrong view. ⚠️ """ print(message) #endif } } else { switch arrowDirection { case .up, .down: contentSize = verticalContentSize case .left, .right: contentSize = horizontalContentSize } } } // arrow point // This point is in the coordinate system of container view. do { var point = CGPoint.zero switch arrowDirection { case .up: point.x = arrowReferRect.midX point.y = arrowReferRect.maxY case .down: point.x = arrowReferRect.midX point.y = arrowReferRect.minY case .left: point.x = arrowReferRect.maxX point.y = arrowReferRect.midY case .right: point.x = arrowReferRect.minX point.y = arrowReferRect.midY } // If the point is on the edge, a little adjustment is required for improving the visual effect. let arrowPointSafeRect: CGRect = { var rect = safeContainerRect.insetBy(dx: cornerRadius, dy: cornerRadius) rect = rect.insetBy(dx: 3.0, dy: 3.0) rect = rect.insetBy(dx: arrowSize.width / 2, dy: arrowSize.width / 2) return rect }() if !arrowPointSafeRect.contains(point) { switch arrowDirection { case .up, .down: if point.x < arrowPointSafeRect.minX { point.x = arrowPointSafeRect.minX } if point.x > arrowPointSafeRect.maxX { point.x = arrowPointSafeRect.maxX } case .left, .right: if point.y < arrowPointSafeRect.minY { point.y = arrowPointSafeRect.minY } if point.y > arrowPointSafeRect.maxY { point.y = arrowPointSafeRect.maxY } } } arrowPoint = point } // The frame of the popover view in the container view. let frame: CGRect = { let size: CGSize = { var width: CGFloat = contentSize.width, height: CGFloat = contentSize.height switch arrowDirection { case .up, .down: height += arrowSize.height case .left, .right: width += arrowSize.height } return .init(width: width, height: height) }() var origin = CGPoint.zero switch arrowDirection { case .up, .down: origin.x = { var x = arrowPoint.x - size.width / 2 if arrowPoint.x <= safeContainerRect.midX { x = max(x, safeContainerRect.minX) } if arrowPoint.x > safeContainerRect.midX { x = min(x, safeContainerRect.maxX - size.width) } return x }() case .left, .right: origin.y = { var y = arrowPoint.y - size.height / 2 if arrowPoint.y <= safeContainerRect.midY { y = max(y, safeContainerRect.minY) } if arrowPoint.y > safeContainerRect.midY { y = min(y, safeContainerRect.maxY - size.height) } return y }() } switch arrowDirection { case .up: origin.y = arrowPoint.y case .down: origin.y = arrowPoint.y - size.height case .left: origin.x = arrowPoint.x case .right: origin.x = arrowPoint.x - size.width } return .init(origin: origin, size: size) }() self.frame = frame // background view if let view = dataSource?.backgroundView(for: self) { popoverContainerView.addSubview(view) backgroundView = view view.inner.makeMarginConstraints(to: popoverContainerView) } // content view if let view = dataSource?.contentView(for: self) { popoverContainerView.addSubview(view) contentView = view var frame = CGRect.zero frame.size = realContentSize switch arrowDirection { case .up: frame.origin.x = (contentSize.width - realContentSize.width) / 2 frame.origin.y = arrowSize.height + (contentSize.height - realContentSize.height) / 2 case .left: frame.origin.x = arrowSize.height + (contentSize.width - realContentSize.width) / 2 frame.origin.y = (contentSize.height - realContentSize.height) / 2 case .down, .right: frame.origin.x = (contentSize.width - realContentSize.width) / 2 frame.origin.y = (contentSize.height - realContentSize.height) / 2 } view.frame = frame } // draw popover view do { let arrowPointInPopover = CGPoint(x: arrowPoint.x - frame.minX, y: arrowPoint.y - frame.minY) var context = FSPopoverDrawContext() context.showsArrow = showsArrow context.arrowSize = arrowSize context.arrowPoint = arrowPointInPopover context.arrowDirection = arrowDirection context.cornerRadius = cornerRadius context.contentSize = contentSize context.popoverSize = frame.size context.borderWidth = borderWidth context.borderColor = borderColor context.shadowColor = shadowColor context.shadowRadius = shadowRadius context.shadowOpacity = shadowOpacity let drawer = FSPopoverDrawer(context: context) // mask do { let path = drawer.generatePath() let maskLayer = CAShapeLayer() maskLayer.path = path.cgPath popoverContainerView.layer.mask = maskLayer } // shadow if let image = drawer.generateShadowImage() { let layer = CALayer() layer.contents = image.cgImage self.layer.addSublayer(layer) self.shadowLayer = layer layer.frame.size = image.size layer.frame.origin.x = (frame.width - image.size.width) / 2 layer.frame.origin.y = (frame.height - image.size.height) / 2 } // border if let image = drawer.generateBorderImage() { let layer = CALayer() layer.contents = image.cgImage self.layer.addSublayer(layer) self.borderLayer = layer layer.frame.size = image.size layer.frame.origin.x = (frame.width - image.size.width) / 2 layer.frame.origin.y = (frame.height - image.size.height) / 2 } } } func p_present(from rect: CGRect, in view: UIView? = nil, displayIn specifiedView: UIView? = nil, animated: Bool = true, completion: (() -> Void)? = nil) { p_mainThreadCheck() p_prepareForDisplaying() let displayView: UIView = { /// NOTE: /// When displaying in the UIScrollView, container view can not get the correct size. /// So ignore scroll view here. if let view = specifiedView, !(view is UIScrollView), view.bounds.width > 0, view.bounds.height > 0.0 { return view } let window = p_createDisplayWindow() window.bounds = .init(origin: .zero, size: UIScreen.main.bounds.size) window.isHidden = false displayWindow = window return window }() arrowReferRect = { if let view = view { return view.convert(rect, to: displayView) } return rect }() let containerView = p_createContainerView() do { p_setContainerView(containerView) displayView.addSubview(containerView) containerView.inner.makeMarginConstraints(to: displayView) displayView.layoutIfNeeded() } containerSize = containerView.bounds.size setNeedsReload() // This operation will cause the method `layoutSubviews()` to be called, and then // the method `reloadDataIfNeeded()` will be called, so it's unnecessary to call // `reloadDataIfNeeded()` explicitly here. containerView.addSubview(self) // Layout the popover view for the animation of the next step. containerView.layoutIfNeeded() let completedHandler: (() -> Void) = { [unowned self] in self.isFreezing = false completion?() } if animated, let transitioning = transitioningDelegate { let context = transitionContext(for: .present) context.onDidCompleteTransition = completedHandler transitioning.animateTransition(transitionContext: context) } else { completedHandler() } } func p_dismiss(animated: Bool = true, isSelection: Bool = false, completion: (() -> Void)? = nil) { p_mainThreadCheck() // Can not do anything when the popover view begins disappearing. isFreezing = true containerView?.isUserInteractionEnabled = false let completedHandler: (() -> Void) = { [unowned self] in self.isFreezing = false self.removeFromSuperview() self.containerView?.removeFromSuperview() self.p_destroyDisplayWindow() completion?() } if animated, let transitioning = transitioningDelegate { let context = transitionContext(for: .dismiss(isSelection)) context.onDidCompleteTransition = completedHandler transitioning.animateTransition(transitionContext: context) } else { completedHandler() } } } // MARK: - Methods/Actions private extension FSPopoverView { @objc func p_handleTap() { guard !isFreezing, // Can not do anything while the popover view is freezing. dataSource?.popoverViewShouldDismissOnTapOutside(self) ?? true else { return } p_dismiss() } } // MARK: - Methods/Public public extension FSPopoverView { /// Call this method if you want to bring the popover view to the front of /// the view displaying the popover view. func moveToFront() { guard let containerView = containerView, let superview = containerView.superview else { return } superview.bringSubviewToFront(containerView) } /// Call this method to get the maximum size in the direction. /// /// Sometimes, the size of your content view may need to be adjusted to fit the popover view. /// Such as your content view is a table view with a lot of cells, and the popover view doesn't /// have enough space for the table view to show all of the cells, this requires the data source /// object to return a proper content size. In this time, you need a size as a reference, and /// this is where this method in handy. /// /// - Important: /// * Returns nil if the popover view is not yet ready for presenting, so it's recommended that /// call this method in the content size method of data source object. /// func maximumContentSizeOf(direction: FSPopoverView.ArrowDirection) -> CGSize? { guard containerSize.width > 0, containerSize.height > 0 else { return nil } let arrowSize = p_getArrowSize() let referRect = arrowReferRect.insetBy(dx: -arrowSize.height, dy: -arrowSize.height) let safeAreaInsets = dataSource?.containerSafeAreaInsets(for: self) ?? .zero let safeContainerRect = CGRect(origin: .zero, size: containerSize).inset(by: safeAreaInsets) switch direction { case .up: return CGSize(width: safeContainerRect.width, height: max(0.0, safeContainerRect.maxY - referRect.maxY)) case .down: return CGSize(width: safeContainerRect.width, height: max(0.0, referRect.minY - safeContainerRect.minY)) case .left: return CGSize(width: max(0.0, safeContainerRect.maxX - referRect.maxX), height: safeContainerRect.height) case .right: return CGSize(width: max(0.0, referRect.minX - safeContainerRect.minX), height: safeContainerRect.height) } } }