jsdw_ios/Pods/Popover/Classes/Popover.swift

686 lines
23 KiB
Swift

//
// Popover.swift
// Popover
//
// Created by corin8823 on 8/16/15.
// Copyright (c) 2015 corin8823. All rights reserved.
//
import Foundation
import UIKit
public enum PopoverOption {
case arrowSize(CGSize)
case animationIn(TimeInterval)
case animationOut(TimeInterval)
case cornerRadius(CGFloat)
case sideEdge(CGFloat)
case blackOverlayColor(UIColor)
case overlayBlur(UIBlurEffect.Style)
case type(PopoverType)
case color(UIColor)
case dismissOnBlackOverlayTap(Bool)
case showBlackOverlay(Bool)
case springDamping(CGFloat)
case initialSpringVelocity(CGFloat)
case sideOffset(CGFloat)
case borderColor(UIColor)
}
@objc public enum PopoverType: Int {
case up
case down
case left
case right
case auto
}
@objcMembers
open class Popover: UIView {
// custom property
open var arrowSize: CGSize = CGSize(width: 16.0, height: 10.0)
open var animationIn: TimeInterval = 0.6
open var animationOut: TimeInterval = 0.3
open var cornerRadius: CGFloat = 6.0
open var sideEdge: CGFloat = 20.0
open var popoverType: PopoverType = .down
open var blackOverlayColor: UIColor = UIColor(white: 0.0, alpha: 0.2)
open var overlayBlur: UIBlurEffect?
open var popoverColor: UIColor = UIColor.white
open var dismissOnBlackOverlayTap: Bool = true
open var showBlackOverlay: Bool = true
open var highlightFromView: Bool = false
open var highlightCornerRadius: CGFloat = 0
open var springDamping: CGFloat = 0.7
open var initialSpringVelocity: CGFloat = 3
open var sideOffset: CGFloat = 6.0
open var borderColor: UIColor?
// custom closure
open var willShowHandler: (() -> ())?
open var willDismissHandler: (() -> ())?
open var didShowHandler: (() -> ())?
open var didDismissHandler: (() -> ())?
public fileprivate(set) var blackOverlay: UIControl = UIControl()
fileprivate var containerView: UIView!
fileprivate var contentView: UIView!
fileprivate var contentViewFrame: CGRect!
fileprivate var arrowShowPoint: CGPoint!
public init() {
super.init(frame: .zero)
self.backgroundColor = .clear
self.accessibilityViewIsModal = true
}
public init(showHandler: (() -> ())?, dismissHandler: (() -> ())?) {
super.init(frame: .zero)
self.backgroundColor = .clear
self.didShowHandler = showHandler
self.didDismissHandler = dismissHandler
self.accessibilityViewIsModal = true
}
public init(options: [PopoverOption]?, showHandler: (() -> ())? = nil, dismissHandler: (() -> ())? = nil) {
super.init(frame: .zero)
self.backgroundColor = .clear
self.setOptions(options)
self.didShowHandler = showHandler
self.didDismissHandler = dismissHandler
self.accessibilityViewIsModal = true
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func layoutSubviews() {
super.layoutSubviews()
self.contentView.frame = self.bounds
}
open func showAsDialog(_ contentView: UIView) {
guard let rootView = UIApplication.shared.keyWindow else {
return
}
self.showAsDialog(contentView, inView: rootView)
}
open func showAsDialog(_ contentView: UIView, inView: UIView) {
self.arrowSize = .zero
let point = CGPoint(x: inView.center.x,
y: inView.center.y - contentView.frame.height / 2)
self.show(contentView, point: point, inView: inView)
}
open func show(_ contentView: UIView, fromView: UIView) {
guard let rootView = UIApplication.shared.keyWindow else {
return
}
self.show(contentView, fromView: fromView, inView: rootView)
}
open func show(_ contentView: UIView, fromView: UIView, inView: UIView) {
let point: CGPoint
//TODO: add left/right auto
if self.popoverType == .auto {
if let point = fromView.superview?.convert(fromView.frame.origin, to: nil),
point.y + fromView.frame.height + self.arrowSize.height + contentView.frame.height > inView.frame.height {
self.popoverType = .up
} else {
self.popoverType = .down
}
}
switch self.popoverType {
case .up:
point = inView.convert(
CGPoint(
x: fromView.frame.origin.x + (fromView.frame.size.width / 2),
y: fromView.frame.origin.y
), from: fromView.superview)
case .down, .auto:
point = inView.convert(
CGPoint(
x: fromView.frame.origin.x + (fromView.frame.size.width / 2),
y: fromView.frame.origin.y + fromView.frame.size.height
), from: fromView.superview)
case .left:
point = inView.convert(
CGPoint(x: fromView.frame.origin.x - sideOffset,
y: fromView.frame.origin.y + 0.5 * fromView.frame.height
), from: fromView.superview)
case .right:
point = inView.convert(
CGPoint(x: fromView.frame.origin.x + fromView.frame.size.width + sideOffset,
y: fromView.frame.origin.y + 0.5 * fromView.frame.height
), from: fromView.superview)
}
if self.highlightFromView {
self.createHighlightLayer(fromView: fromView, inView: inView)
}
self.show(contentView, point: point, inView: inView)
}
open func show(_ contentView: UIView, point: CGPoint) {
guard let rootView = UIApplication.shared.keyWindow else {
return
}
self.show(contentView, point: point, inView: rootView)
}
open func show(_ contentView: UIView, point: CGPoint, inView: UIView) {
if self.dismissOnBlackOverlayTap || self.showBlackOverlay {
self.blackOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.blackOverlay.frame = inView.bounds
inView.addSubview(self.blackOverlay)
if showBlackOverlay {
if let overlayBlur = self.overlayBlur {
let effectView = UIVisualEffectView(effect: overlayBlur)
effectView.frame = self.blackOverlay.bounds
effectView.isUserInteractionEnabled = false
self.blackOverlay.addSubview(effectView)
} else {
if !self.highlightFromView {
self.blackOverlay.backgroundColor = self.blackOverlayColor
}
self.blackOverlay.alpha = 0
}
}
if self.dismissOnBlackOverlayTap {
self.blackOverlay.addTarget(self, action: #selector(Popover.dismiss), for: .touchUpInside)
}
}
self.containerView = inView
self.contentView = contentView
self.contentView.backgroundColor = UIColor.clear
self.contentView.layer.cornerRadius = self.cornerRadius
self.contentView.layer.masksToBounds = true
self.arrowShowPoint = point
self.show()
}
open override func accessibilityPerformEscape() -> Bool {
self.dismiss()
return true
}
@objc open func dismiss() {
if self.superview != nil {
self.willDismissHandler?()
UIView.animate(withDuration: self.animationOut, delay: 0,
options: UIView.AnimationOptions(),
animations: {
self.transform = CGAffineTransform(scaleX: 0.0001, y: 0.0001)
self.blackOverlay.alpha = 0
}){ _ in
self.contentView.removeFromSuperview()
self.blackOverlay.removeFromSuperview()
self.removeFromSuperview()
self.transform = CGAffineTransform.identity
self.didDismissHandler?()
}
}
}
override open func draw(_ rect: CGRect) {
super.draw(rect)
let arrow = UIBezierPath()
let color = self.popoverColor
let arrowPoint = self.containerView.convert(self.arrowShowPoint, to: self)
switch self.popoverType {
case .up:
arrow.move(to: CGPoint(x: arrowPoint.x, y: self.bounds.height))
arrow.addLine(
to: CGPoint(
x: arrowPoint.x - self.arrowSize.width * 0.5,
y: self.isCornerLeftArrow ? self.arrowSize.height : self.bounds.height - self.arrowSize.height
)
)
arrow.addLine(to: CGPoint(x: self.cornerRadius, y: self.bounds.height - self.arrowSize.height))
arrow.addArc(
withCenter: CGPoint(
x: self.cornerRadius,
y: self.bounds.height - self.arrowSize.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(90),
endAngle: self.radians(180),
clockwise: true)
arrow.addLine(to: CGPoint(x: 0, y: self.cornerRadius))
arrow.addArc(
withCenter: CGPoint(
x: self.cornerRadius,
y: self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(180),
endAngle: self.radians(270),
clockwise: true)
arrow.addLine(to: CGPoint(x: self.bounds.width - self.cornerRadius, y: 0))
arrow.addArc(
withCenter: CGPoint(
x: self.bounds.width - self.cornerRadius,
y: self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(270),
endAngle: self.radians(0),
clockwise: true)
arrow.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height - self.arrowSize.height - self.cornerRadius))
arrow.addArc(
withCenter: CGPoint(
x: self.bounds.width - self.cornerRadius,
y: self.bounds.height - self.arrowSize.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(0),
endAngle: self.radians(90),
clockwise: true)
arrow.addLine(
to: CGPoint(
x: arrowPoint.x + self.arrowSize.width * 0.5,
y: self.isCornerRightArrow ? self.arrowSize.height : self.bounds.height - self.arrowSize.height
)
)
case .down, .auto:
arrow.move(to: CGPoint(x: arrowPoint.x, y: 0))
if self.isCloseToCornerRightArrow && !self.isCornerRightArrow {
if !isBehindCornerRightArrow {
arrow.addLine(to: CGPoint(x: self.bounds.width - self.cornerRadius, y: self.arrowSize.height))
arrow.addArc(
withCenter: CGPoint(x: self.bounds.width - self.cornerRadius, y: self.arrowSize.height + self.cornerRadius),
radius: self.cornerRadius,
startAngle: self.radians(270.0),
endAngle: self.radians(0),
clockwise: true)
} else {
arrow.addLine(to: CGPoint(x: self.bounds.width, y: self.arrowSize.height + self.cornerRadius))
arrow.addLine(to: CGPoint(x: self.bounds.width, y: self.arrowSize.height))
}
} else {
arrow.addLine(
to: CGPoint(
x: self.isBehindCornerLeftArrow ? self.frame.minX - self.arrowSize.width * 0.5 : arrowPoint.x + self.arrowSize.width * 0.5,
y: self.isCornerRightArrow ? self.arrowSize.height + self.bounds.height : self.arrowSize.height
)
)
arrow.addLine(to: CGPoint(x: self.bounds.width - self.cornerRadius, y: self.arrowSize.height))
arrow.addArc(
withCenter: CGPoint(
x: self.bounds.width - self.cornerRadius,
y: self.arrowSize.height + self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(270.0),
endAngle: self.radians(0),
clockwise: true)
}
arrow.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height - self.cornerRadius))
arrow.addArc(
withCenter: CGPoint(
x: self.bounds.width - self.cornerRadius,
y: self.bounds.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(0),
endAngle: self.radians(90),
clockwise: true)
arrow.addLine(to: CGPoint(x: 0, y: self.bounds.height))
arrow.addArc(
withCenter: CGPoint(
x: self.cornerRadius,
y: self.bounds.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(90),
endAngle: self.radians(180),
clockwise: true)
arrow.addLine(to: CGPoint(x: 0, y: self.arrowSize.height + self.cornerRadius))
if !isBehindCornerLeftArrow {
arrow.addArc(
withCenter: CGPoint(
x: self.cornerRadius,
y: self.arrowSize.height + self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(180),
endAngle: self.radians(270),
clockwise: true)
}
if isBehindCornerRightArrow {
arrow.addLine(to: CGPoint(
x: self.bounds.width - self.arrowSize.width * 0.5,
y: self.isCornerLeftArrow ? self.arrowSize.height + self.bounds.height : self.arrowSize.height))
} else if isCloseToCornerLeftArrow && !isCornerLeftArrow {
() // skipping this line in that case
} else {
arrow.addLine(to: CGPoint(x: arrowPoint.x - self.arrowSize.width * 0.5,
y: self.isCornerLeftArrow ? self.arrowSize.height + self.bounds.height : self.arrowSize.height))
}
case .left:
arrow.move(to: CGPoint(x: self.bounds.width, y: self.bounds.height * 0.5))
arrow.addLine(
to: CGPoint(
x: self.bounds.width - self.arrowSize.height,
y: self.bounds.height * 0.5 + self.arrowSize.width * 0.5
))
arrow.addLine(to: CGPoint(x:self.bounds.width - self.arrowSize.height, y: self.bounds.height - self.cornerRadius))
arrow.addArc(
withCenter: CGPoint(
x: self.bounds.width - self.arrowSize.height - self.cornerRadius,
y: self.bounds.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(0.0),
endAngle: self.radians(90),
clockwise: true)
arrow.addLine(to: CGPoint(x: self.cornerRadius, y: self.bounds.height))
arrow.addArc(
withCenter: CGPoint(
x: self.cornerRadius,
y: self.bounds.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(90),
endAngle: self.radians(180),
clockwise: true)
arrow.addLine(to: CGPoint(x: 0, y: self.cornerRadius))
arrow.addArc(
withCenter: CGPoint(
x: self.cornerRadius,
y: self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(180),
endAngle: self.radians(270),
clockwise: true)
arrow.addLine(to: CGPoint(x: self.bounds.width - self.arrowSize.height - self.cornerRadius, y: 0))
arrow.addArc(
withCenter: CGPoint(x: self.bounds.width - self.arrowSize.height - self.cornerRadius,
y: self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(270),
endAngle: self.radians(0),
clockwise: true)
arrow.addLine(to: CGPoint(x: self.bounds.width - self.arrowSize.height,
y: self.bounds.height * 0.5 - self.arrowSize.width * 0.5
))
case .right:
arrow.move(to: CGPoint(x: arrowPoint.x, y: self.bounds.height * 0.5))
arrow.addLine(
to: CGPoint(
x: arrowPoint.x + self.arrowSize.height,
y: self.bounds.height * 0.5 + 0.5 * self.arrowSize.width
))
arrow.addLine(
to: CGPoint(
x: arrowPoint.x + self.arrowSize.height,
y: self.bounds.height - self.cornerRadius
))
arrow.addArc(
withCenter: CGPoint(
x: arrowPoint.x + self.arrowSize.height + self.cornerRadius,
y: self.bounds.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(180.0),
endAngle: self.radians(90),
clockwise: false)
arrow.addLine(to: CGPoint(x: self.bounds.width + arrowPoint.x - self.cornerRadius, y: self.bounds.height))
arrow.addArc(
withCenter: CGPoint(
x: self.bounds.width + arrowPoint.x - self.cornerRadius,
y: self.bounds.height - self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(90),
endAngle: self.radians(0),
clockwise: false)
arrow.addLine(to: CGPoint(x: self.bounds.width + arrowPoint.x, y: self.cornerRadius))
arrow.addArc(
withCenter: CGPoint(
x: self.bounds.width + arrowPoint.x - self.cornerRadius,
y: self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(0),
endAngle: self.radians(-90),
clockwise: false)
arrow.addLine(to: CGPoint(x: arrowPoint.x + self.arrowSize.height - self.cornerRadius, y: 0))
arrow.addArc(
withCenter: CGPoint(x: arrowPoint.x + self.arrowSize.height + self.cornerRadius,
y: self.cornerRadius
),
radius: self.cornerRadius,
startAngle: self.radians(-90),
endAngle: self.radians(-180),
clockwise: false)
arrow.addLine(to: CGPoint(x: arrowPoint.x + self.arrowSize.height,
y: self.bounds.height * 0.5 - self.arrowSize.width * 0.5))
}
color.setFill()
arrow.fill()
if let borderColor = borderColor {
borderColor.setStroke()
arrow.stroke()
}
}
}
private extension Popover {
func setOptions(_ options: [PopoverOption]?){
if let options = options {
for option in options {
switch option {
case let .arrowSize(value):
self.arrowSize = value
case let .animationIn(value):
self.animationIn = value
case let .animationOut(value):
self.animationOut = value
case let .cornerRadius(value):
self.cornerRadius = value
case let .sideEdge(value):
self.sideEdge = value
case let .blackOverlayColor(value):
self.blackOverlayColor = value
case let .overlayBlur(style):
self.overlayBlur = UIBlurEffect(style: style)
case let .type(value):
self.popoverType = value
case let .color(value):
self.popoverColor = value
case let .dismissOnBlackOverlayTap(value):
self.dismissOnBlackOverlayTap = value
case let .showBlackOverlay(value):
self.showBlackOverlay = value
case let .springDamping(value):
self.springDamping = value
case let .initialSpringVelocity(value):
self.initialSpringVelocity = value
case let .sideOffset(value):
self.sideOffset = value
case let .borderColor(value):
self.borderColor = value
}
}
}
}
func create() {
var frame = self.contentView.frame
switch self.popoverType {
case .up, .down, .auto:
frame.origin.x = self.arrowShowPoint.x - frame.size.width * 0.5
case .left, .right:
frame.origin.y = self.arrowShowPoint.y - frame.size.height * 0.5
}
var sideEdge: CGFloat = 0.0
if frame.size.width < self.containerView.frame.size.width {
sideEdge = self.sideEdge
}
let outerSideEdge = frame.maxX - self.containerView.bounds.size.width
if outerSideEdge > 0 {
frame.origin.x -= (outerSideEdge + sideEdge)
} else {
if frame.minX < 0 {
frame.origin.x += abs(frame.minX) + sideEdge
}
}
self.frame = frame
let arrowPoint = self.containerView.convert(self.arrowShowPoint, to: self)
var anchorPoint: CGPoint
switch self.popoverType {
case .up:
frame.origin.y = self.arrowShowPoint.y - frame.height - self.arrowSize.height
anchorPoint = CGPoint(x: arrowPoint.x / frame.size.width, y: 1)
case .down, .auto:
frame.origin.y = self.arrowShowPoint.y
anchorPoint = CGPoint(x: arrowPoint.x / frame.size.width, y: 0)
case .left:
frame.origin.x = self.arrowShowPoint.x - frame.size.width - self.arrowSize.height
anchorPoint = CGPoint(x: 1, y: 0.5)
case .right:
frame.origin.x = self.arrowShowPoint.x
anchorPoint = CGPoint(x: 0, y: 0.5)
}
if self.arrowSize == .zero {
anchorPoint = CGPoint(x: 0.5, y: 0.5)
}
let lastAnchor = self.layer.anchorPoint
self.layer.anchorPoint = anchorPoint
let x = self.layer.position.x + (anchorPoint.x - lastAnchor.x) * self.layer.bounds.size.width
let y = self.layer.position.y + (anchorPoint.y - lastAnchor.y) * self.layer.bounds.size.height
self.layer.position = CGPoint(x: x, y: y)
switch self.popoverType {
case .up, .down, .auto:
frame.size.height += self.arrowSize.height
case .left, .right:
frame.size.width += self.arrowSize.height
}
self.frame = frame
}
func createHighlightLayer(fromView: UIView, inView: UIView) {
let path = UIBezierPath(rect: inView.bounds)
let highlightRect = inView.convert(fromView.frame, from: fromView.superview)
let highlightPath = UIBezierPath(roundedRect: highlightRect, cornerRadius: self.highlightCornerRadius)
path.append(highlightPath)
path.usesEvenOddFillRule = true
let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = CAShapeLayerFillRule.evenOdd
fillLayer.fillColor = self.blackOverlayColor.cgColor
self.blackOverlay.layer.addSublayer(fillLayer)
}
func show() {
self.setNeedsDisplay()
switch self.popoverType {
case .up:
self.contentView.frame.origin.y = 0.0
case .down, .auto:
self.contentView.frame.origin.y = self.arrowSize.height
case .left, .right:
self.contentView.frame.origin.x = 0
}
self.addSubview(self.contentView)
self.containerView.addSubview(self)
self.create()
self.transform = CGAffineTransform(scaleX: 0.0, y: 0.0)
self.willShowHandler?()
UIView.animate(
withDuration: self.animationIn,
delay: 0,
usingSpringWithDamping: self.springDamping,
initialSpringVelocity: self.initialSpringVelocity,
options: UIView.AnimationOptions(),
animations: {
self.transform = CGAffineTransform.identity
}){ _ in
self.didShowHandler?()
}
UIView.animate(
withDuration: self.animationIn / 3,
delay: 0,
options: .curveLinear,
animations: {
self.blackOverlay.alpha = 1
}, completion: nil)
}
var isCloseToCornerLeftArrow: Bool {
return self.arrowShowPoint.x < self.frame.origin.x + arrowSize.width/2 + cornerRadius
}
var isCloseToCornerRightArrow: Bool {
return self.arrowShowPoint.x > (self.frame.origin.x + self.bounds.width) - arrowSize.width/2 - cornerRadius
}
var isCornerLeftArrow: Bool {
return self.arrowShowPoint.x == self.frame.origin.x
}
var isCornerRightArrow: Bool {
return self.arrowShowPoint.x == self.frame.origin.x + self.bounds.width
}
var isBehindCornerLeftArrow: Bool {
return self.arrowShowPoint.x < self.frame.origin.x
}
var isBehindCornerRightArrow: Bool {
return self.arrowShowPoint.x > self.frame.origin.x + self.bounds.width
}
func radians(_ degrees: CGFloat) -> CGFloat {
return CGFloat.pi * degrees / 180
}
}