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函数的写法