swift PopTip

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了swift PopTip相关的知识,希望对你有一定的参考价值。

import UIKit

/// Enum that specifies the direction of the poptip
public enum PopTipDirection: Int {
    /// Up, the poptip will appear above the element, arrow pointing down
    case up
    /// Down, the poptip will appear below the element, arrow pointing up
    case down
    /// Left, the poptip will appear on the left of the element, arrow pointing right
    case left
    /// Right, the poptip will appear on the right of the element, arrow pointing left
    case right
    /// None, the poptip will appear above the element with no arrow
    case none
}

/** Enum that specifies the type of entrance animation. Entrance animations are performed while showing the poptip.

 - `scale`: The poptip scales from 0% to 100%
 - `transitions`: The poptip moves in position from the edge of the screen
 - `fadeIn`: The poptip fade in
 - `custom`: The Animation is provided by the user
 - `none`: No Animation
 */
public enum PopTipEntranceAnimation {
    /// The poptip scales from 0% to 100%
    case scale
    /// The poptip moves in position from the edge of the screen
    case transition
    /// The poptip fades in
    case fadeIn
    /// The Animation is provided by the user
    case custom
    /// No Animation
    case none
}

/** Enum that specifies the type of entrance animation. Entrance animations are performed while showing the poptip.

 - `scale`: The poptip scales from 100% to 0%
 - `fadeOut`: The poptip fade out
 - `custom`: The Animation is provided by the user
 - `none`: No Animation
 */
public enum PopTipExitAnimation {
    /// The poptip scales from 100% to 0%
    case scale
    /// The poptip fades out
    case fadeOut
    /// The Animation is provided by the user
    case custom
    /// No Animation
    case none
}

/** Enum that specifies the type of action animation. Action animations are performed after the poptip is visible and the entrance animation completed.

 - `bounce(offset: CGFloat?)`: The poptip bounces following its direction. The bounce offset can be provided optionally
 - `float(offset: CGFloat?)`: The poptip floats in place. The float offset can be provided optionally
 - `pulse(offset: CGFloat?)`: The poptip pulsates by changing its size. The maximum amount of pulse increase can be provided optionally
 - `none`: No animation
 */
public enum PopTipActionAnimation {
    /// The poptip bounces following its direction. The bounce offset can be provided optionally
    case bounce(CGFloat?)
    /// The poptip floats in place. The float offset can be provided optionally. Defaults to 8 points
    case float(CGFloat?)
    /// The poptip pulsates by changing its size. The maximum amount of pulse increase can be provided optionally. Defaults to 1.1 (110% of the original size)
    case pulse(CGFloat?)
    /// No animation
    case none
}

private let DefaultBounceOffset = CGFloat(8)
private let DefaultFloatOffset = CGFloat(8)
private let DefaultPulseOffset = CGFloat(1.1)

open class PopTip: UIView {

    // MARK: - Public Properties

    /// The text displayed by the poptip. Can be updated once the poptip is visible
    open var text: String? {
        didSet {
            accessibilityLabel = text
            setNeedsLayout()
        }
    }

    /// The `UIFont` used in the poptip's text
    open var font = UIFont.systemFont(ofSize: UIFont.systemFontSize)

    /// The `UIColor` of the text
    open dynamic var textColor = UIColor.white

    /// The `NSTextAlignment` of the text
    open dynamic var textAlignment = NSTextAlignment.center

    /// The `UIColor` for the poptip's background
    open dynamic var bubbleColor = UIColor(red: 67 / 255.0, green: 78 / 255.0, blue: 111 / 255.0, alpha: 0.95)

    /// The `UIColor` for the poptip's bordedr
    open dynamic var borderColor = UIColor.clear

    /// The width for the poptip's border
    open dynamic var borderWidth = CGFloat(0.0)

    /// The `Double` with the poptip's border radius
    open dynamic var cornerRadius = CGFloat(8.0)

    /// The `BOOL` that determines wether the poptip is rounded. If set to `true` the radius will equal `frame.height / 2`
    open dynamic var isRounded = false

    /// Holds the offset between the poptip and origin
    open dynamic var offset = CGFloat(6.0)

    /// Holds the CGFloat with the padding used for the inner text
    open dynamic var padding = CGFloat(6.0)

    /// Holds the insets setting for padding different direction
    open dynamic var edgeInsets = UIEdgeInsets.zero

    /// Holds the CGSize with the width and height of the arrow
    open dynamic var arrowSize = CGSize(width: 16, height: 8)

    /// Holds the NSTimeInterval with the duration of the revealing animation
    open dynamic var animationIn: TimeInterval = 0.4

    /// Holds the NSTimeInterval with the duration of the disappearing animation
    open dynamic var animationOut: TimeInterval = 0.2

    /// Holds the NSTimeInterval with the delay of the revealing animation
    open dynamic var delayIn: TimeInterval = 0

    /// Holds the NSTimeInterval with the delay of the disappearing animation
    open dynamic var delayOut: TimeInterval = 0

    /// Holds the enum with the type of entrance animation (triggered once the poptip is shown)
    open var entranceAnimation = PopTipEntranceAnimation.scale

    /// Holds the enum with the type of exit animation (triggered once the poptip is dismissed)
    open var exitAnimation = PopTipExitAnimation.scale

    /// Holds the enum with the type of action animation (triggered once the poptip is shown)
    open var actionAnimation = PopTipActionAnimation.none

    /// Holds the NSTimeInterval with the duration of the action animation
    open dynamic var actionAnimationIn: TimeInterval = 1.2

    /// Holds the NSTimeInterval with the duration of the action stop animation
    open dynamic var actionAnimationOut: TimeInterval = 1.0

    /// Holds the NSTimeInterval with the delay of the action animation
    open dynamic var actionDelayIn: TimeInterval = 0

    /// Holds the NSTimeInterval with the delay of the action animation stop
    open dynamic var actionDelayOut: TimeInterval = 0

    /// CGfloat value that determines the leftmost margin from the screen
    open dynamic var edgeMargin = CGFloat(6.0)

    /// Holds the offset between the bubble and origin
    open dynamic var bubbleOffset = CGFloat(0.0)

    /// Color of the mask that is going to dim the background when the pop up is visible
    open dynamic var maskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)

    /// Flag to enable or disable background mask
    open dynamic var shouldShowMask = false

    /// Holds the CGrect with the rect the tip is pointing to
    open var from = CGRect.zero {
        didSet {
            setup()
        }
    }

    /// Holds the readonly BOOL with the poptip visiblity. The poptip is considered visible as soon as
    /// the animation is complete, and invisible when the subview is removed from its parent.
    open var isVisible: Bool { get { return self.superview != nil } }

    /// A boolean value that determines whether the poptip is dismissed on tap.
    open dynamic var shouldDismissOnTap = false

    /// A boolean value that determines whether to dismiss when tapping or swiping outside the poptip.
    open dynamic var shouldDismissOnTouchOutside = true

    /// A boolean value that determines whether to dismiss when tapping outside the poptip.
    open dynamic var shouldDismissOnTapOutside = false

    /// A boolean value that determines whether to dismiss when swiping outside the poptip.
    open dynamic var shouldDismissOnSwipeOutside = false

    /// A boolean value that determines if the action animation should start automatically when the poptip is shown
    open dynamic var startActionAnimationOnShow = true

    /// A direction that determines what swipe direction to dismiss when swiping outside the poptip.

    /// The default direction is `right`
    open var swipeRemoveGestureDirection = UISwipeGestureRecognizerDirection.right

    /// A block that will be fired when the user taps the poptip.
    open var tapHandler: ((PopTip) -> Void)?

    /// A block that will be fired when the poptip appears.
    open var appearHandler: ((PopTip) -> Void)?

    /// A block that will be fired when the poptip is dismissed.
    open var dismissHandler: ((PopTip) -> Void)?

    /// A block that handles the entrance animation of the poptip. Should be provided
    /// when using a `PopTipActionAnimationCustom` entrance animation type.
    /// Please note that the poptip will be automatically added as a subview before firing the block
    /// Remember to call the completion block provided
    open var entranceAnimationHandler: ((@escaping (Void) -> Void) -> Void)?

    /// A block block that handles the exit animation of the poptip. Should be provided
    /// when using a `AMPopTipActionAnimationCustom` exit animation type.
    /// Remember to call the completion block provided
    open var exitAnimationHandler: ((@escaping (Void) -> Void) -> Void)?


    // MARK: - Private Properties

    /// The CGPoint originating the arrow. Read only.
    open private(set) var arrowPosition = CGPoint.zero

    /// A read only reference to the view containing the poptip
    open private(set) var containerView: UIView?

    /// The direction from which the poptip is shown. Read only.
    open private(set) var direction = PopTipDirection.none

    /// Holds the readonly BOOL with the poptip animation state.
    open private(set) var isAnimating: Bool = false

    /// The view that dims the background (including the button that triggered PopTip.
    /// The mask by appears with fade in effect only.
    open private(set) var backgroundMask: UIView?

    fileprivate var attributedText: NSAttributedString?
    fileprivate var paragraphStyle = NSMutableParagraphStyle()
    fileprivate var tapGestureRecognizer: UITapGestureRecognizer?
    fileprivate var tapRemoveGestureRecognizer: UITapGestureRecognizer?
    fileprivate var swipeGestureRecognizer: UISwipeGestureRecognizer?
    fileprivate var dismissTimer: Timer?
    fileprivate var textBounds = CGRect.zero
    fileprivate var maxWidth = CGFloat(0)
    fileprivate var customView: UIView?
    fileprivate var isApplicationInBackground: Bool?
    fileprivate var label: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        return label
    }()
    private var shouldBounce = false

    // MARK: -

    /// Setup a poptip oriented vertically (direction .up or .down). Returns the bubble frame and the arrow position
    ///
    /// - Returns: a tuple with the bubble frame and the arrow position
    internal func setupVertically() -> (CGRect, CGPoint) {
        guard let containerView = containerView else { return (CGRect.zero, CGPoint.zero) }

        var frame = CGRect.zero
        let offset = self.offset * (direction == .up ? -1 : 1)

        frame.size = CGSize(width: textBounds.width + padding * 2 + edgeInsets.horizontal, height: textBounds.height + padding * 2 + edgeInsets.vertical + arrowSize.height)
        var x = from.origin.x + from.width / 2 - frame.width / 2
        if x < 0 { x = edgeMargin }
        if (x + frame.width > containerView.bounds.width) { x = containerView.bounds.width - frame.width - edgeMargin }

        if direction == .down {
            frame.origin = CGPoint(x: x, y: from.origin.y + from.height + offset)
        } else {
            frame.origin = CGPoint(x: x, y: from.origin.y - frame.height + offset)
        }

        // Make sure that the bubble doesn't leave the boundaries of the view
        let arrowPosition = CGPoint(
            x: from.origin.x + from.width / 2 - frame.origin.x,
            y: (direction == .up) ? frame.height : from.origin.y + from.height - frame.origin.y + offset
        )

        if bubbleOffset > 0 && arrowPosition.x < bubbleOffset {
            bubbleOffset = arrowPosition.x - arrowSize.width
        } else if bubbleOffset < 0 && frame.width < fabs(bubbleOffset) {
            bubbleOffset = -(arrowPosition.x - arrowSize.width)
        } else if bubbleOffset < 0 && (frame.origin.x - arrowPosition.x) < fabs(bubbleOffset) {
            bubbleOffset = -(arrowSize.width + edgeMargin)
        }

        // Make sure that the bubble doesn't leaves the boundaries of the view
        let leftSpace = frame.origin.x - containerView.frame.origin.x
        let rightSpace = containerView.frame.width - leftSpace - frame.width

        if bubbleOffset < 0 && leftSpace < fabs(bubbleOffset) {
            bubbleOffset = -leftSpace + edgeMargin
        } else if bubbleOffset > 0 && rightSpace < bubbleOffset {
            bubbleOffset = rightSpace - edgeMargin
        }

        frame.origin.x += bubbleOffset
        frame.size = CGSize(width: frame.width + borderWidth * 2, height: frame.height + borderWidth * 2)

        return (frame, arrowPosition)
    }

    /// Setup a poptip oriented horizontally (direction .left or .right). Returns the bubble frame and the arrow position
    ///
    /// - Returns: a tuple with the bubble frame and the arrow position
    internal func setupHorizontally() -> (CGRect, CGPoint) {
        guard let containerView = containerView else { return (CGRect.zero, CGPoint.zero) }

        var frame = CGRect.zero
        let offset = self.offset * (direction == .left ? -1 : 1)
        frame.size = CGSize(width: textBounds.width + padding * 2 + edgeInsets.horizontal + arrowSize.height, height: textBounds.height + padding * 2 + edgeInsets.vertical)

        let x = direction == .left ? from.origin.x - frame.width + offset : from.origin.x + from.width + offset
        var y = from.origin.y + from.height / 2 - frame.height / 2

        if y < 0 { y = edgeMargin }
        if y + frame.height > containerView.bounds.height { y = containerView.bounds.height - frame.height - edgeMargin }
        frame.origin = CGPoint(x: x, y: y)

        // Make sure that the bubble doesn't leave the boundaries of the view
        let arrowPosition = CGPoint(
            x: direction == .left ? from.origin.x - frame.origin.x + offset : from.origin.x + from.width - frame.origin.x + offset,
            y: from.origin.y + from.height / 2 - frame.origin.y
        )

        if bubbleOffset > 0 && arrowPosition.y < bubbleOffset {
            bubbleOffset = arrowPosition.y - arrowSize.width
        } else if bubbleOffset < 0 && frame.height < fabs(bubbleOffset) {
            bubbleOffset = -(arrowPosition.y - arrowSize.height)
        }

        let topSpace = frame.origin.y - containerView.frame.origin.y
        let bottomSpace = containerView.frame.height - topSpace - frame.height

        if bubbleOffset < 0 && topSpace < fabs(bubbleOffset) {
            bubbleOffset = -topSpace + edgeMargin
        } else if bubbleOffset > 0 && bottomSpace < bubbleOffset {
            bubbleOffset = bottomSpace - edgeMargin
        }

        frame.origin.y += bubbleOffset
        frame.size = CGSize(width: frame.width + borderWidth * 2, height: frame.height + borderWidth * 2)

        return (frame, arrowPosition)
    }

    fileprivate func textBounds(for text: String?, attributedText: NSAttributedString?, view: UIView?, with font: UIFont, padding: CGFloat, edges: UIEdgeInsets, in maxWidth: CGFloat) -> CGRect {
        var bounds = CGRect.zero
        if let text = text {
            bounds = NSString(string: text).boundingRect(with: CGSize(width: maxWidth, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
        }
        if let attributedText = attributedText {
            bounds = attributedText.boundingRect(with: CGSize(width: maxWidth, height: CGFloat.infinity), options: .usesLineFragmentOrigin, context: nil)
        }
        if let view = view {
            bounds = view.frame
        }
        bounds.origin = CGPoint(x: padding + edges.left, y: padding + edges.top)
        return bounds.integral
    }

    fileprivate func setup() {
        guard let containerView = containerView else { return }

        var rect = CGRect.zero
        backgroundColor = .clear

        if direction == .left {
            maxWidth = CGFloat.minimum(maxWidth, from.origin.x - padding * 2 - edgeInsets.horizontal - arrowSize.width)
        }
        if direction == .right {
            maxWidth = CGFloat.minimum(maxWidth, containerView.bounds.width - from.origin.x - from.width - padding * 2 - edgeInsets.horizontal - arrowSize.width)
        }

        textBounds = textBounds(for: text, attributedText: attributedText, view: customView, with: font, padding: padding, edges: edgeInsets, in: maxWidth)

        switch direction {
        case .up:
            let dimensions = setupVertically()
            rect = dimensions.0
            arrowPosition = dimensions.1
            let anchor = arrowPosition.x / rect.size.width
            layer.anchorPoint = CGPoint(x: anchor, y: 1)
            layer.position = CGPoint(x: layer.position.x + rect.width * anchor, y: layer.position.y + rect.height / 2)
        case .down:
            let dimensions = setupVertically()
            rect = dimensions.0
            arrowPosition = dimensions.1
            let anchor = arrowPosition.x / rect.size.width
            textBounds.origin = CGPoint(x: textBounds.origin.x, y: textBounds.origin.y + arrowSize.height)
            layer.anchorPoint = CGPoint(x: anchor, y: 0)
            layer.position = CGPoint(x: layer.position.x + rect.width * anchor, y: layer.position.y - rect.height / 2)
        case .left:
            let dimensions = setupHorizontally()
            rect = dimensions.0
            arrowPosition = dimensions.1
            let anchor = arrowPosition.y / rect.height
            layer.anchorPoint = CGPoint(x: 1, y: anchor)
            layer.position = CGPoint(x: layer.position.x - rect.width / 2, y: layer.position.y + rect.height * anchor)
        case .right:
            let dimensions = setupHorizontally()
            rect = dimensions.0
            arrowPosition = dimensions.1
            textBounds.origin = CGPoint(x: textBounds.origin.x + arrowSize.height, y: textBounds.origin.y)
            let anchor = arrowPosition.y / rect.height
            layer.anchorPoint = CGPoint(x: 0, y: anchor)
            layer.position = CGPoint(x: layer.position.x + rect.width / 2, y: layer.position.y + rect.height * anchor)
        case .none:
            rect.size = CGSize(width: textBounds.width + padding * 2.0 + edgeInsets.horizontal + borderWidth * 2, height: textBounds.height + padding * 2.0 + edgeInsets.vertical + borderWidth * 2)
            rect.origin = CGPoint(x: from.midX - rect.size.width / 2, y: from.midY - rect.height / 2 - offset)
            arrowPosition = CGPoint.zero
            layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
            layer.position = CGPoint(x: from.midX, y: from.midY)
        }

        label.frame = textBounds
        if label.superview == nil {
            addSubview(label)
        }

        frame = rect

        if let customView = customView {
            customView.frame = textBounds
        }

        if !shouldShowMask {
            backgroundMask?.removeFromSuperview()
        } else {
            if backgroundMask == nil {
                backgroundMask = UIView()
                backgroundMask?.backgroundColor = maskColor
            }
            backgroundMask?.frame = containerView.bounds
        }

        setNeedsDisplay()

        if tapGestureRecognizer == nil {
            tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(PopTip.handleTap(_:)))
            tapGestureRecognizer?.cancelsTouchesInView = true
            self.addGestureRecognizer(tapGestureRecognizer ?? UITapGestureRecognizer())
        }
        if tapRemoveGestureRecognizer == nil {
            tapRemoveGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(PopTip.hide))
        }
        if swipeGestureRecognizer == nil {
            swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(PopTip.hide))
        }

        if isApplicationInBackground == nil {
            NotificationCenter.default.addObserver(self, selector: #selector(PopTip.handleApplicationActive), name: Notification.Name.UIApplicationDidBecomeActive, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(PopTip.handleApplicationResignActive), name: Notification.Name.UIApplicationWillResignActive, object: nil)
        }
    }

    /// Custom draw override
    ///
    /// - Parameter rect: the rect occupied by the view
    open override func draw(_ rect: CGRect) {
        if isRounded {
            let showHorizontally = direction == .left || direction == .right
            cornerRadius = (frame.size.height - (showHorizontally ? 0 : arrowSize.height)) / 2
        }

        let path = PopTip.pathWith(rect: rect, frame: frame, direction: direction, arrowSize: arrowSize, arrowPosition: arrowPosition, border: borderWidth, radius: cornerRadius)

        bubbleColor.setFill()
        path.fill()

        borderColor.setStroke()
        path.lineWidth = borderWidth
        path.stroke()

        paragraphStyle.alignment = textAlignment

        if let text = text {
            let titleAttributes: [String : Any] = [
                NSParagraphStyleAttributeName: paragraphStyle,
                NSFontAttributeName: font,
                NSForegroundColorAttributeName: textColor
            ]

            label.attributedText = NSAttributedString(string: text, attributes: titleAttributes)
        } else if let text = attributedText {
            label.attributedText = text
        } else {
            label.attributedText = nil
        }
    }

    /// Shows an animated poptip in a given view, from a given rectangle. The property `isVisible` will be `true` as soon as the poptip is added to the given view.
    ///
    /// - Parameters:
    ///   - text: The text to display
    ///   - direction: The direction of the poptip in relation to the element that generates it
    ///   - maxWidth: The maximum width of the poptip. If the poptip won't fit in the given space, this will be overridden.
    ///   - view: The view that will hold the poptip as a subview.
    ///   - frame: The originating frame. The poptip's arrow will point to the center of this frame.
    ///   - duration: Optional time interval that determines when the poptip will self-dismiss.
    open func show(text: String, direction: PopTipDirection, maxWidth: CGFloat, in view: UIView, from frame: CGRect, duration: TimeInterval? = nil) {
        resetView()

        attributedText = nil
        self.text = text
        accessibilityLabel = text
        self.direction = direction
        containerView = view
        self.maxWidth = maxWidth
        customView?.removeFromSuperview()
        customView = nil
        from = frame

        show(duration: duration)
    }

    /// Shows an animated poptip in a given view, from a given rectangle. The property `isVisible` will be `true` as soon as the poptip is added to the given view.
    ///
    /// - Parameters:
    ///   - attributedText: The attributed string to display
    ///   - direction: The direction of the poptip in relation to the element that generates it
    ///   - maxWidth: The maximum width of the poptip. If the poptip won't fit in the given space, this will be overridden.
    ///   - view: The view that will hold the poptip as a subview.
    ///   - frame: The originating frame. The poptip's arrow will point to the center of this frame.
    ///   - duration: Optional time interval that determines when the poptip will self-dismiss.
    open func show(attributedText: NSAttributedString, direction: PopTipDirection, maxWidth: CGFloat, in view: UIView, from frame: CGRect, duration: TimeInterval? = nil) {
        resetView()

        text = nil
        self.attributedText = attributedText
        accessibilityLabel = attributedText.string
        self.direction = direction
        containerView = view
        self.maxWidth = maxWidth
        customView?.removeFromSuperview()
        customView = nil
        from = frame

        show(duration: duration)
    }

    /// Shows an animated poptip in a given view, from a given rectangle. The property `isVisible` will be `true` as soon as the poptip is added to the given view.
    ///
    /// - Parameters:
    ///   - customView: A custom view
    ///   - direction: The direction of the poptip in relation to the element that generates it
    ///   - view: The view that will hold the poptip as a subview.
    ///   - frame: The originating frame. The poptip's arrow will point to the center of this frame.
    ///   - duration: Optional time interval that determines when the poptip will self-dismiss.
    open func show(customView: UIView, direction: PopTipDirection, in view: UIView, from frame: CGRect, duration: TimeInterval? = nil) {
        resetView()

        text = nil
        attributedText = nil
        self.direction = direction
        containerView = view
        maxWidth = customView.frame.size.width
        self.customView?.removeFromSuperview()
        self.customView = customView
        self.addSubview(customView)
        customView.layoutIfNeeded()
        from = frame

        show(duration: duration)
    }

    /// Update the current text
    ///
    /// - Parameter text: the new text
    open func update(text: String) {
        self.text = text
        updateBubble()
    }

    /// Update the current text
    ///
    /// - Parameter attributedText: the new attributs string
    open func update(attributedText: NSAttributedString) {
        self.attributedText = attributedText
        updateBubble()
    }

    /// Update the current text
    ///
    /// - Parameter customView: the new custom view
    open func update(customView: UIView) {
        self.customView = customView
        updateBubble()
    }

    /// Hides the poptip and removes it from the view. The property `isVisible` will be set to `false` when the animation is complete and the poptip is removed from the parent view.
    ///
    /// - Parameter forced: Force the removal, ignoring running animations
    open func hide(forced: Bool = false) {
        if !forced && isAnimating {
            return
        }

        resetView()
        isAnimating = true
        dismissTimer?.invalidate()
        dismissTimer = nil

        if let gestureRecognizer = tapRemoveGestureRecognizer {
            containerView?.removeGestureRecognizer(gestureRecognizer)
        }
        if let gestureRecognizer = swipeGestureRecognizer {
            containerView?.removeGestureRecognizer(gestureRecognizer)
        }

        let completion = {
            self.customView?.removeFromSuperview()
            self.customView = nil
            self.dismissActionAnimation()
            self.backgroundMask?.removeFromSuperview()
            self.removeFromSuperview()
            self.layer.removeAllAnimations()
            self.transform = .identity
            self.isAnimating = false
            self.dismissHandler?(self)
        }

        if isApplicationInBackground ?? false {
            completion()
        } else {
            performExitAnimation(completion: completion)
        }
    }

    /// Makes the poptip perform the action indefinitely. The action animation calls for the user's attention after the poptip is shown
    open func startActionAnimation() {
        performActionAnimation()
    }

    /// Stops the poptip action animation. Does nothing if the poptip wasn't animating in the first place.
    ///
    /// - Parameter completion: Optional completion block clled once the animation is completed
    open func stopActionAnimation(_ completion: ((Void) -> Void)? = nil) {
        dismissActionAnimation(completion)
    }

    fileprivate func resetView() {
        layer.removeAllAnimations()
        transform = .identity
        shouldBounce = false
    }

    fileprivate func updateBubble() {
        stopActionAnimation {
            UIView.animate(withDuration: 0.2, delay: 0, options: [.transitionCrossDissolve, .beginFromCurrentState], animations: {
                self.setup()
            }) { (_) in
                self.startActionAnimation()
            }
        }
    }

    fileprivate func show(duration: TimeInterval? = nil) {
        isAnimating = true
        dismissTimer?.invalidate()

        setNeedsLayout()
        performEntranceAnimation {
            if self.shouldDismissOnTapOutside {
                self.containerView?.addGestureRecognizer(self.tapRemoveGestureRecognizer ?? UITapGestureRecognizer())
            }
            if self.shouldDismissOnSwipeOutside {
                self.containerView?.addGestureRecognizer(self.swipeGestureRecognizer ?? UITapGestureRecognizer())
            }
            self.appearHandler?(self)
            if self.startActionAnimationOnShow {
                self.performActionAnimation()
            }
            self.isAnimating = false
            if let duration = duration {
                self.dismissTimer = Timer.scheduledTimer(timeInterval: duration, target: self, selector: #selector(PopTip.hide), userInfo: nil, repeats: false)
            }
        }
    }

    @objc fileprivate func handleTap(_ gesture: UITapGestureRecognizer) {
        if shouldDismissOnTap {
            hide()
        }
        tapHandler?(self)
    }

    @objc fileprivate func handleApplicationActive() {
        isApplicationInBackground = false
    }

    @objc fileprivate func handleApplicationResignActive() {
        isApplicationInBackground = true
    }

    fileprivate func performActionAnimation() {
        switch actionAnimation {
        case .bounce(let offset):
            shouldBounce = true
            bounceAnimation(offset: offset ?? DefaultBounceOffset)
        case .float(let offset):
            floatAnimation(offset: offset ?? DefaultFloatOffset)
        case .pulse(let offset):
            pulseAnimation(offset: offset ?? DefaultPulseOffset)
        case .none:
            return
        }
    }

    fileprivate func dismissActionAnimation(_ completion: ((Void) -> Void)? = nil) {
        shouldBounce = false
        UIView.animate(withDuration: actionAnimationOut / 2, delay: actionDelayOut, options: .beginFromCurrentState, animations: {
            self.transform = .identity
        }) { (_) in
            self.layer.removeAllAnimations()
            completion?()
        }
    }

    fileprivate func bounceAnimation(offset: CGFloat) {
        var offsetX = CGFloat(0)
        var offsetY = CGFloat(0)
        switch direction {
        case .up, .none:
            offsetY = -offset
        case .left:
            offsetX = -offset
        case .right:
            offsetX = offset
        case .down:
            offsetY = offset
        }

        UIView.animate(withDuration: actionAnimationIn / 10, delay: actionDelayIn, options: [.curveEaseIn, .allowUserInteraction], animations: {
            self.transform = CGAffineTransform(translationX: offsetX, y: offsetY)
        }) { _ in
            UIView.animate(withDuration: self.actionAnimationIn - self.actionAnimationIn / 10, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 1, options: .allowUserInteraction, animations: {
                self.transform = .identity
            }, completion: { done in
                if self.shouldBounce && done {
                    self.bounceAnimation(offset: offset)
                }
            })
        }
    }

    fileprivate func floatAnimation(offset: CGFloat) {
        var offsetX = offset
        var offsetY = offset
        switch direction {
        case .up, .none:
            offsetY = -offset
        case .left:
            offsetX = -offset
        default: break
        }

        UIView.animate(withDuration: actionAnimationIn / 2, delay: actionDelayIn, options: [.curveEaseInOut, .repeat, .autoreverse, .beginFromCurrentState, .allowUserInteraction], animations: {
            self.transform = CGAffineTransform(translationX: offsetX, y: offsetY)
        }, completion: nil)
    }

    fileprivate func pulseAnimation(offset: CGFloat) {
        UIView.animate(withDuration: actionAnimationIn / 2, delay: actionDelayIn, options: [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState, .autoreverse, .repeat], animations: {
            self.transform = CGAffineTransform(scaleX: offset, y: offset)
        }, completion: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

}

fileprivate extension UIEdgeInsets {
    var horizontal: CGFloat {
        return self.left + self.right
    }

    var vertical: CGFloat {
        return self.top + self.bottom
    }
}

// MARK: - Draw Bubble
public extension PopTip {

    class func pathWith(rect: CGRect, frame: CGRect, direction: PopTipDirection, arrowSize: CGSize, arrowPosition: CGPoint, border: CGFloat = 0, radius: CGFloat = 0) -> UIBezierPath {
        var path = UIBezierPath()
        var balloon = CGRect.zero

        switch direction {
        case .none:
            balloon = CGRect(x: border, y: border, width: frame.width - border * 2, height: frame.height - border * 2)

            path = UIBezierPath(roundedRect: balloon, cornerRadius: radius)

        case .down:
            balloon = CGRect(x: 0, y: arrowSize.height, width: rect.width - border * 2, height: rect.height - arrowSize.height - border * 2)

            path.move(to: CGPoint(x: arrowPosition.x + border, y: arrowPosition.y))
            path.addLine(to: CGPoint(x: border + arrowPosition.x + arrowSize.width / 2, y: arrowPosition.y + arrowSize.height))
            path.addLine(to: CGPoint(x: balloon.width - radius, y: arrowSize.height))
            path.addArc(withCenter: CGPoint(x: balloon.width - radius, y: arrowSize.height + radius) , radius: radius, startAngle: radians(270), endAngle: radians(0), clockwise: true)
            path.addLine(to: CGPoint(x: balloon.width, y: arrowSize.height + balloon.height - radius))
            path.addArc(withCenter: CGPoint(x: balloon.width - radius, y: arrowSize.height + balloon.height - radius), radius: radius, startAngle: radians(0), endAngle: radians(90), clockwise: true)
            path.addLine(to: CGPoint(x: border + radius, y: arrowSize.height + balloon.height))
            path.addArc(withCenter: CGPoint(x: border + radius, y: arrowSize.height + balloon.height - radius), radius: radius, startAngle: radians(90), endAngle: radians(180), clockwise: true)
            path.addLine(to: CGPoint(x: border, y: arrowSize.height + radius))
            path.addArc(withCenter: CGPoint(x: border + radius, y: arrowSize.height + radius), radius: radius, startAngle: radians(180), endAngle: radians(270), clockwise: true)
            path.addLine(to: CGPoint(x: border + arrowPosition.x - arrowSize.width / 2, y: arrowPosition.y + arrowSize.height))
            path.close()

        case .up:
            balloon = CGRect(x: 0, y: 0, width: rect.size.width - border * 2, height: rect.size.height - arrowSize.height - border * 2)

            path.move(to: CGPoint(x: arrowPosition.x + border, y: arrowPosition.y - border))
            path.addLine(to: CGPoint(x: border + arrowPosition.x + arrowSize.width / 2, y: arrowPosition.y - arrowSize.height - border))
            path.addLine(to: CGPoint(x: balloon.width - radius, y: balloon.maxY + border))
            path.addArc(withCenter: CGPoint(x: balloon.width - radius, y: balloon.maxY - radius + border), radius:radius, startAngle:radians(90), endAngle:radians(0), clockwise:false)
            path.addLine(to: CGPoint(x: balloon.width, y: balloon.minY + radius + border))
            path.addArc(withCenter: CGPoint(x: balloon.width - radius, y: balloon.minY + radius + border), radius:radius, startAngle:radians(0), endAngle:radians(270), clockwise: false)
            path.addLine(to: CGPoint(x: border + radius, y: balloon.minY + border))
            path.addArc(withCenter: CGPoint(x: border + radius, y: balloon.minY + radius + border), radius:radius, startAngle:radians(270), endAngle:radians(180), clockwise: false)
            path.addLine(to: CGPoint(x: border, y: balloon.maxY - radius + border))
            path.addArc(withCenter: CGPoint(x: border + radius, y: balloon.maxY - radius + border), radius:radius, startAngle:radians(180), endAngle:radians(90), clockwise: false)
            path.addLine(to: CGPoint(x: border + arrowPosition.x - arrowSize.width / 2, y: arrowPosition.y - arrowSize.height - border))
            path.close()

        case .left:
            balloon = CGRect(x: 0, y: 0, width: rect.size.width - arrowSize.height - border * 2, height: rect.size.height - border * 2)

            path.move(to: CGPoint(x: arrowPosition.x - border, y: arrowPosition.y))
            path.addLine(to: CGPoint(x: arrowPosition.x - arrowSize.height - border, y: arrowPosition.y - arrowSize.width / 2))
            path.addLine(to: CGPoint(x: balloon.width - border, y: balloon.minY + radius))
            path.addArc(withCenter: CGPoint(x: balloon.width - radius - border, y: balloon.minY + radius + border), radius:radius, startAngle:radians(0), endAngle:radians(270), clockwise: false)
            path.addLine(to: CGPoint(x: radius + border, y: balloon.minY + border))
            path.addArc(withCenter: CGPoint(x: radius + border, y: balloon.minY + radius + border), radius:radius, startAngle:radians(270), endAngle:radians(180), clockwise: false)
            path.addLine(to: CGPoint(x: border, y: balloon.maxY - radius - border))
            path.addArc(withCenter: CGPoint(x: radius + border, y: balloon.maxY - radius - border), radius:radius, startAngle:radians(180), endAngle:radians(90), clockwise: false)
            path.addLine(to: CGPoint(x: balloon.width - radius - border, y: balloon.maxY - border))
            path.addArc(withCenter: CGPoint(x: balloon.width - radius -  border, y: balloon.maxY - radius -  border), radius:radius, startAngle:radians(90), endAngle:radians(0), clockwise: false)
            path.addLine(to: CGPoint(x: arrowPosition.x - arrowSize.height - border, y: arrowPosition.y + arrowSize.width / 2))
            path.close()

        case .right:
            balloon = CGRect(x: arrowSize.height, y: 0, width: rect.size.width - arrowSize.height - border * 2, height: rect.size.height - border * 2)

            path.move(to: CGPoint(x: arrowPosition.x + border, y: arrowPosition.y))
            path.addLine(to: CGPoint(x: arrowPosition.x + arrowSize.height + border, y: arrowPosition.y - arrowSize.width / 2))
            path.addLine(to: CGPoint(x: balloon.minX + border, y: balloon.minY + radius + border))
            path.addArc(withCenter: CGPoint(x: balloon.minX + radius + border, y: balloon.minY + radius + border), radius:radius, startAngle:radians(180), endAngle:radians(270), clockwise: true)
            path.addLine(to: CGPoint(x: balloon.minX + balloon.width - radius - border, y: balloon.minY + border))
            path.addArc(withCenter: CGPoint(x: balloon.minX + balloon.width - radius - border, y: balloon.minY + radius + border), radius:radius, startAngle:radians(270), endAngle:radians(0), clockwise: true)
            path.addLine(to: CGPoint(x: balloon.minX + balloon.width - border, y: balloon.maxY - radius - border))
            path.addArc(withCenter: CGPoint(x: balloon.minX + balloon.width - radius - border, y: balloon.maxY - radius - border), radius:radius, startAngle:radians(0), endAngle:radians(90), clockwise: true)
            path.addLine(to: CGPoint(x: balloon.minX + radius + border, y: balloon.maxY - border))
            path.addArc(withCenter: CGPoint(x: balloon.minX + radius + border, y: balloon.maxY - radius - border), radius:radius, startAngle:radians(90), endAngle:radians(180), clockwise: true)
            path.addLine(to: CGPoint(x: arrowPosition.x + arrowSize.height + border, y: arrowPosition.y + arrowSize.width / 2))
            path.close()
        }

        return path
    }

}

fileprivate func radians(_ degrees: CGFloat) -> CGFloat {
    return (CGFloat.pi * degrees) / 180
}

// MARK: - Transitions
public extension PopTip {

    /// Triggers the chosen entrance animation
    ///
    /// - Parameter completion: the completion handler
    public func performEntranceAnimation(completion: @escaping () -> Void) {
        switch entranceAnimation {
        case .scale:
            entranceScale(completion: completion)
        case .transition:
            entranceTransition(completion: completion)
        case .fadeIn:
            entranceFadeIn(completion: completion)
        case .custom:
            if let backgroundMask = backgroundMask {
                containerView?.addSubview(backgroundMask)
            }
            containerView?.addSubview(self)
            entranceAnimationHandler?(completion)
        case .none:
            if let backgroundMask = backgroundMask {
                containerView?.addSubview(backgroundMask)
            }
            containerView?.addSubview(self)
            completion()
        }
    }

    /// Triggers the chosen exit animation
    ///
    /// - Parameter completion: the completion handler
    public func performExitAnimation(completion: @escaping () -> Void) {
        switch exitAnimation {
        case .scale:
            exitScale(completion: completion)
        case .fadeOut:
            exitFadeOut(completion: completion)
        case .custom:
            containerView?.addSubview(self)
            exitAnimationHandler?(completion)
        case .none:
            containerView?.addSubview(self)
            completion()
        }
    }

    private func entranceTransition(completion: @escaping () -> Void) {
        transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
        switch direction {
        case .up:
            transform = transform.translatedBy(x: 0, y: -from.origin.y)
        case .down, .none:
            transform = transform.translatedBy(x: 0, y: (containerView?.frame.height ?? 0) - from.origin.y)
        case .left:
            transform = transform.translatedBy(x: from.origin.x, y: 0)
        case .right:
            transform = transform.translatedBy(x: (containerView?.frame.width ?? 0) - from.origin.x, y: 0)
        }
        if let backgroundMask = backgroundMask {
            containerView?.addSubview(backgroundMask)
        }
        containerView?.addSubview(self)

        UIView.animate(withDuration: animationIn, delay: delayIn, usingSpringWithDamping: 0.6, initialSpringVelocity: 1.5, options: [.curveEaseInOut, .beginFromCurrentState], animations: {
            self.transform = .identity
            self.backgroundMask?.alpha = 1
        }) { _ in
            completion()
        }
    }

    private func entranceScale(completion: @escaping (Void) -> Void) {
        transform = CGAffineTransform(scaleX: 0, y: 0)
        if let backgroundMask = backgroundMask {
            containerView?.addSubview(backgroundMask)
        }
        containerView?.addSubview(self)

        UIView.animate(withDuration: animationIn, delay: delayIn, usingSpringWithDamping: 0.6, initialSpringVelocity: 1.5, options: [.curveEaseInOut, .beginFromCurrentState], animations: {
            self.transform = .identity
            self.backgroundMask?.alpha = 1
        }) { _ in
            completion()
        }
    }

    private func entranceFadeIn(completion: @escaping () -> Void) {
        if let backgroundMask = backgroundMask {
            containerView?.addSubview(backgroundMask)
        }
        containerView?.addSubview(self)

        alpha = 0
        UIView.animate(withDuration: animationIn, delay: delayIn, options: [.curveEaseInOut, .beginFromCurrentState], animations: {
            self.alpha = 1
            self.backgroundMask?.alpha = 1
        }) { _ in
            completion()
        }
    }

    private func exitScale(completion: @escaping () -> Void) {
        transform = .identity

        UIView.animate(withDuration: animationOut, delay: delayOut, options: [.curveEaseInOut, .beginFromCurrentState], animations: {
            self.transform = CGAffineTransform(scaleX: 0.0001, y: 0.0001)
            self.backgroundMask?.alpha = 0
        }) { _ in
            completion()
        }
    }

    private func exitFadeOut(completion: @escaping () -> Void) {
        alpha = 0

        UIView.animate(withDuration: animationOut, delay: delayOut, options: [.curveEaseInOut, .beginFromCurrentState], animations: {
            self.alpha = 0
            self.backgroundMask?.alpha = 0
        }) { _ in
            completion()
        }
    }

}

// MARK: - Customize
extension PopTip {

    open func show(view: UIView, in container: UIView, from frame: CGRect) {
        if let optionalWindow = UIApplication.shared.delegate?.window, let window = optionalWindow {
            let rect = container.convert(frame, to: window)
            let direction = self.getDirection(of: view, from: rect, in: window)
            self.show(customView: view, direction: direction, in: window, from: rect, duration: nil)
        } else {
            let direction = self.getDirection(of: view, from: frame, in: container)
            self.show(customView: view, direction: direction, in: container, from: frame, duration: nil)
        }
    }

    private func getDirection(of view: UIView, from frame: CGRect, in container: UIView) -> PopTipDirection {
        let popHeight = view.frame.height + edgeInsets.top + edgeInsets.bottom + padding * 2 + borderWidth * 2
        if frame.minY - popHeight - arrowSize.height - 64 > 0 {
            return .up
        }
        if container.frame.height - frame.maxY - popHeight - arrowSize.height > 0 {
            return .down
        }

        let popWidth = view.frame.width + edgeInsets.left + edgeInsets.right + padding * 2 + borderWidth * 2
        if container.frame.width - frame.maxX - popWidth - arrowSize.height > 0 {
            return .right
        }
        if frame.minX - popWidth - arrowSize.height > 0 {
            return .left
        }

        return .none
    }

    open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if self.point(inside: point, with: event) {
            for subview in subviews {
                let subPoint = self.convert(point, to: subview)
                if let view = subview.hitTest(subPoint, with: event) {
                    return view
                }
            }
        }

        if shouldDismissOnTouchOutside {
            self.hide()
        }

        return nil
    }

}

以上是关于swift PopTip的主要内容,如果未能解决你的问题,请参考以下文章

iview 在Table组件render 中使用Poptip组件 阿星小栈

iview的table中点击Icon弹Poptip,render函数的写法

iview的poptip插件,自动换行与自定义content的内容

iview上的兼容性问题

iview table内渲染proptip组件

将 Node.js 换成 Go