Organic Maps sources as of 02.04.2025 (fad26bbf22ac3da75e01e62aa01e5c8e11861005)

To expand with full Organic Maps and Maps.ME commits history run:
  git remote add om-historic [om-historic.git repo url]
  git fetch --tags om-historic
  git replace squashed-history historic-commits
This commit is contained in:
Konstantin Pastbin
2025-04-13 16:37:30 +07:00
commit e3e4a1985a
12931 changed files with 13195100 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
import UIKit
struct ChartLineInfo {
let color: UIColor
let point: CGPoint
let formattedValue: String
}
protocol ChartInfoViewDelegate: AnyObject {
func chartInfoView(_ view: ChartInfoView, infoAtPointX pointX: CGFloat) -> (String, [ChartLineInfo])?
func chartInfoView(_ view: ChartInfoView, didCaptureInfoView captured: Bool)
func chartInfoView(_ view: ChartInfoView, didMoveToPoint pointX: CGFloat)
}
class ChartInfoView: ExpandedTouchView {
weak var delegate: ChartInfoViewDelegate?
private let pointInfoView = ChartPointInfoView()
private let pointsView = ChartPointIntersectionView(frame: CGRect(x: 0, y: 0, width: 2, height: 0))
private let myPositionView = ChartMyPositionView(frame: CGRect(x: 0, y: 0, width: 2, height: 0))
private var lineInfo: ChartLineInfo?
fileprivate var captured = false {
didSet {
delegate?.chartInfoView(self, didCaptureInfoView: captured)
}
}
private var _infoX: CGFloat = 0
var infoX: CGFloat {
get { _infoX }
set {
_infoX = newValue
update(bounds.width * _infoX)
}
}
var myPositionX: CGFloat = -1 {
didSet {
if myPositionX < 0 || myPositionX > 1 {
myPositionView.isHidden = true
return
}
myPositionView.isHidden = false
updateMyPosition()
}
}
var tooltipBackgroundColor: UIColor = UIColor.white {
didSet {
pointInfoView.backgroundColor = tooltipBackgroundColor
}
}
var font: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) {
didSet {
pointInfoView.font = font
}
}
var textColor: UIColor = UIColor.black {
didSet {
pointInfoView.textColor = textColor
}
}
public var infoBackgroundColor: UIColor = UIColor.white {
didSet {
pointInfoView.backgroundColor = infoBackgroundColor
}
}
public var infoShadowColor: UIColor = UIColor.black {
didSet {
pointInfoView.layer.shadowColor = infoShadowColor.cgColor
}
}
public var infoShadowOpacity: Float = 0.25 {
didSet {
pointInfoView.layer.shadowOpacity = infoShadowOpacity
}
}
var panGR: UIPanGestureRecognizer!
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(myPositionView)
myPositionView.isHidden = true
addSubview(pointsView)
addSubview(pointInfoView)
isExclusiveTouch = true
panGR = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
panGR.delegate = self
addGestureRecognizer(panGR)
pointInfoView.textColor = textColor
pointInfoView.backgroundColor = tooltipBackgroundColor
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
func update(_ x: CGFloat? = nil) {
guard bounds.width > 0 else { return }
let x = x ?? pointsView.center.x
_infoX = x / bounds.width
guard let delegate = delegate,
let (label, intersectionPoints) = delegate.chartInfoView(self, infoAtPointX: x) else { return }
lineInfo = intersectionPoints[0]
pointsView.updatePoint(lineInfo!)
pointInfoView.update(x: x, label: label, points: intersectionPoints)
updateViews(point: lineInfo!.point)
}
private func updateMyPosition() {
myPositionView.center = CGPoint(x: bounds.width * myPositionX, y: myPositionView.center.y)
guard let (_, myPositionPoints) = delegate?.chartInfoView(self, infoAtPointX: myPositionView.center.x) else { return }
myPositionView.pinY = myPositionPoints[0].point.y
}
@objc func onPan(_ sender: UIPanGestureRecognizer) {
let x = sender.location(in: self).x
switch sender.state {
case .possible:
break
case .began:
guard let lineInfo = lineInfo else { return }
captured = abs(x - lineInfo.point.x) <= 22
case .changed:
if captured {
if x < bounds.minX || x > bounds.maxX {
return
}
update(x)
delegate?.chartInfoView(self, didMoveToPoint: x)
}
case .ended, .cancelled, .failed:
captured = false
@unknown default:
fatalError()
}
}
private func updateViews(point: CGPoint) {
pointsView.alpha = 1
pointsView.center = CGPoint(x: point.x, y: bounds.midY)
let s = pointInfoView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
pointInfoView.frame.size = s
let y = max(pointInfoView.frame.height / 2 + 5,
min(bounds.height - pointInfoView.frame.height / 2 - 5, bounds.height - lineInfo!.point.y));
let orientationChangeX = pointInfoView.alignment == .left ? s.width + 40 : bounds.width - s.width - 40
if point.x > orientationChangeX {
pointInfoView.alignment = .left
pointInfoView.center = CGPoint(x: point.x - s.width / 2 - 20, y: y)
} else {
pointInfoView.alignment = .right
pointInfoView.center = CGPoint(x: point.x + s.width / 2 + 20, y: y)
}
var f = pointInfoView.frame
if f.minX < 0 {
f.origin.x = 0
pointInfoView.frame = f
} else if f.minX + f.width > bounds.width {
f.origin.x = bounds.width - f.width
pointInfoView.frame = f
}
let arrowPoint = convert(CGPoint(x: 0, y: bounds.height - lineInfo!.point.y), to: pointInfoView)
pointInfoView.arrowY = arrowPoint.y
}
override func layoutSubviews() {
super.layoutSubviews()
var pf = pointsView.frame
pf.origin.y = bounds.minY
pf.size.height = bounds.height
pointsView.frame = pf
var mf = myPositionView.frame
mf.origin.y = bounds.minY
mf.size.height = bounds.height
myPositionView.frame = mf
update(bounds.width * infoX)
updateMyPosition()
}
}
extension ChartInfoView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return !captured
}
}

View File

@@ -0,0 +1,94 @@
import UIKit
class ChartMyPositionView: UIView {
override class var layerClass: AnyClass { CAShapeLayer.self }
var shapeLayer: CAShapeLayer { layer as! CAShapeLayer }
var pinY: CGFloat = 0 {
didSet {
updatePin()
}
}
fileprivate let pinView = MyPositionPinView(frame: CGRect(x: 0, y: 0, width: 12, height: 16))
override init(frame: CGRect) {
super.init(frame: frame)
shapeLayer.lineDashPattern = [3, 2]
shapeLayer.lineWidth = 2
shapeLayer.strokeColor = UIColor(red: 0.142, green: 0.614, blue: 0.95, alpha: 0.3).cgColor
addSubview(pinView)
transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1)
pinView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.width / 2, y: 0))
path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height))
shapeLayer.path = path.cgPath
updatePin()
}
private func updatePin() {
pinView.center = CGPoint(x: bounds.midX, y: pinY + pinView.bounds.height / 2)
}
}
fileprivate class MyPositionPinView: UIView {
override class var layerClass: AnyClass { CAShapeLayer.self }
var shapeLayer: CAShapeLayer { layer as! CAShapeLayer }
var path: UIBezierPath = {
let p = UIBezierPath()
p.addArc(withCenter: CGPoint(x: 6, y: 6),
radius: 6,
startAngle: -CGFloat.pi / 2,
endAngle: atan(3.5 / 4.8733971724),
clockwise: true)
p.addLine(to: CGPoint(x: 6 + 0.75, y: 15.6614378))
p.addArc(withCenter: CGPoint(x: 6, y: 15),
radius: 1,
startAngle: atan(0.6614378 / 0.75),
endAngle: CGFloat.pi - atan(0.6614378 / 0.75),
clockwise: true)
p.addLine(to: CGPoint(x: 6 - 4.8733971724, y: 9.5))
p.addArc(withCenter: CGPoint(x: 6, y: 6),
radius: 6,
startAngle: CGFloat.pi - atan(3.5 / 4.8733971724),
endAngle: -CGFloat.pi / 2,
clockwise: true)
p.close()
p.append(UIBezierPath(ovalIn: CGRect(x: 3, y: 3, width: 6, height: 6)))
return p
}()
override init(frame: CGRect) {
super.init(frame: frame)
shapeLayer.lineWidth = 0
shapeLayer.fillColor = UIColor(red: 0.142, green: 0.614, blue: 0.95, alpha: 0.5).cgColor
shapeLayer.fillRule = .evenOdd
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let sx = bounds.width / path.bounds.width
let sy = bounds.height / path.bounds.height
let p = path.copy() as! UIBezierPath
p.apply(CGAffineTransform(scaleX: sx, y: sy))
shapeLayer.path = p.cgPath
}
}

View File

@@ -0,0 +1,127 @@
import UIKit
final class ChartPointInfoView: UIView {
enum Alignment {
case left
case right
}
private let distanceLabel = UILabel()
private let altitudeLabel = UILabel()
private let stackView = UIStackView()
private let maskLayer = CAShapeLayer()
private var maskPath: UIBezierPath?
private let isInterfaceRightToLeft = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft
var arrowY: CGFloat? {
didSet {
setNeedsLayout()
}
}
var alignment = Alignment.left {
didSet {
updateMask()
}
}
var font: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) {
didSet {
distanceLabel.font = font
altitudeLabel.font = font
}
}
var textColor: UIColor = .lightGray {
didSet {
distanceLabel.textColor = textColor
altitudeLabel.textColor = textColor
}
}
override var backgroundColor: UIColor? {
didSet {
maskLayer.fillColor = backgroundColor?.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 5
backgroundColor = .clear
layer.shadowColor = UIColor(white: 0, alpha: 1).cgColor
layer.shadowOpacity = 0.25
layer.shadowRadius = 2
layer.shadowOffset = CGSize(width: 0, height: 2)
maskLayer.fillColor = backgroundColor?.cgColor
layer.addSublayer(maskLayer)
stackView.alignment = .leading
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 6),
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 6),
stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -6),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -6)
])
stackView.addArrangedSubview(distanceLabel)
stackView.addArrangedSubview(altitudeLabel)
stackView.setCustomSpacing(6, after: distanceLabel)
distanceLabel.font = font
altitudeLabel.font = font
distanceLabel.textColor = textColor
altitudeLabel.textColor = textColor
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
func set(x: CGFloat, label: String, points: [ChartLineInfo]) {
distanceLabel.text = label
altitudeLabel.text = altitudeText(points[0])
}
func update(x: CGFloat, label: String, points: [ChartLineInfo]) {
distanceLabel.text = label
altitudeLabel.text = altitudeText(points[0])
layoutIfNeeded()
}
private func altitudeText(_ point: ChartLineInfo) -> String {
return String(isInterfaceRightToLeft ? "\(point.formattedValue)" : "\(point.formattedValue)")
}
override func layoutSubviews() {
super.layoutSubviews()
let y = arrowY ?? bounds.midY
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 3)
let trianglePath = UIBezierPath()
trianglePath.move(to: CGPoint(x: bounds.maxX, y: y - 3))
trianglePath.addLine(to: CGPoint(x: bounds.maxX + 5, y: y))
trianglePath.addLine(to: CGPoint(x: bounds.maxX, y: y + 3))
trianglePath.close()
path.append(trianglePath)
maskPath = path
updateMask()
}
private func updateMask() {
guard let path = maskPath?.copy() as? UIBezierPath else { return }
if alignment == .right {
path.apply(CGAffineTransform.identity.scaledBy(x: -1, y: 1).translatedBy(x: -bounds.width, y: 0))
}
maskLayer.path = path.cgPath
layer.shadowPath = path.cgPath
}
}

View File

@@ -0,0 +1,78 @@
import UIKit
fileprivate class CircleView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
var color: UIColor? {
didSet {
shapeLayer.fillColor = color?.withAlphaComponent(0.5).cgColor
ringLayer.fillColor = UIColor.white.cgColor
centerLayer.fillColor = color?.cgColor
}
}
var shapeLayer: CAShapeLayer {
return layer as! CAShapeLayer
}
let ringLayer = CAShapeLayer()
let centerLayer = CAShapeLayer()
override var frame: CGRect {
didSet {
let p = UIBezierPath(ovalIn: bounds)
shapeLayer.path = p.cgPath
ringLayer.frame = shapeLayer.bounds.insetBy(dx: shapeLayer.bounds.width / 6, dy: shapeLayer.bounds.height / 6)
ringLayer.path = UIBezierPath(ovalIn: ringLayer.bounds).cgPath
centerLayer.frame = shapeLayer.bounds.insetBy(dx: shapeLayer.bounds.width / 3, dy: shapeLayer.bounds.height / 3)
centerLayer.path = UIBezierPath(ovalIn: centerLayer.bounds).cgPath
}
}
override init(frame: CGRect) {
super.init(frame: frame)
shapeLayer.fillColor = color?.withAlphaComponent(0.5).cgColor
shapeLayer.lineWidth = 4
shapeLayer.fillRule = .evenOdd
shapeLayer.addSublayer(ringLayer)
shapeLayer.addSublayer(centerLayer)
ringLayer.fillColor = UIColor.white.cgColor
centerLayer.fillColor = color?.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
class ChartPointIntersectionView: UIView {
fileprivate var intersectionView = CircleView()
var color: UIColor = UIColor(red: 0.14, green: 0.61, blue: 0.95, alpha: 1) {
didSet {
intersectionView.color = color
backgroundColor = color.withAlphaComponent(0.5)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = color.withAlphaComponent(0.5)
transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1)
intersectionView.color = color
intersectionView.frame = CGRect(x: 0, y: 0, width: 24, height: 24)
intersectionView.center = CGPoint(x: bounds.midX, y: bounds.midY)
addSubview(intersectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
func updatePoint(_ point: ChartLineInfo) {
intersectionView.center = CGPoint(x: bounds.midX, y: point.point.y)
color = point.color
}
}

View File

@@ -0,0 +1,112 @@
import UIKit
class ChartLineView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
private var minX = 0
private var maxX = 0
private var minY: CGFloat = 0
private var maxY: CGFloat = 0
var isPreview = false
var lineWidth: CGFloat = 1 {
didSet {
shapeLayer.lineWidth = lineWidth
}
}
var chartLine: ChartPresentationLine! {
didSet {
guard let chartLine = chartLine else { return }
maxX = chartLine.values.count - 1
minY = chartLine.minY
maxY = chartLine.maxY
switch chartLine.type {
case .line:
shapeLayer.strokeColor = chartLine.color.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineJoin = .round
case .lineArea:
shapeLayer.strokeColor = UIColor.clear.cgColor
shapeLayer.fillColor = chartLine.color.cgColor
}
shapeLayer.lineWidth = lineWidth
updateGraph()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1)
isUserInteractionEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
var shapeLayer: CAShapeLayer {
return layer as! CAShapeLayer
}
func setViewport(minX: Int, maxX: Int, minY: CGFloat, maxY: CGFloat, animationStyle: ChartAnimation = .none) {
assert(minX < maxX && minY < maxY)
self.minX = minX
self.maxX = maxX
self.minY = minY
self.maxY = maxY
updateGraph(animationStyle: animationStyle)
}
func setX(min: Int, max: Int) {
assert(min < max)
minX = min
maxX = max
updateGraph()
}
func setY(min: CGFloat, max: CGFloat, animationStyle: ChartAnimation = .none) {
assert(min < max)
minY = min
maxY = max
updateGraph(animationStyle: animationStyle)
}
private func updateGraph(animationStyle: ChartAnimation = .none) {
let p = isPreview ? chartLine.previewPath : chartLine.path
guard let realPath = p.copy() as? UIBezierPath else { return }
let xScale = bounds.width / CGFloat(maxX - minX)
let xTranslate = -bounds.width * CGFloat(minX) / CGFloat(maxX - minX)
let yScale = (bounds.height - 1) / CGFloat(maxY - minY)
let yTranslate = (bounds.height - 1) * CGFloat(chartLine.minY - minY) / CGFloat(maxY - minY) + 0.5
let scale = CGAffineTransform.identity.scaledBy(x: xScale, y: yScale)
let translate = CGAffineTransform.identity.translatedBy(x: xTranslate, y: yTranslate)
let transform = scale.concatenating(translate)
realPath.apply(transform)
if animationStyle != .none {
let timingFunction = CAMediaTimingFunction(name: animationStyle == .interactive ? .linear : .easeInEaseOut)
if shapeLayer.animationKeys()?.contains("path") ?? false,
let presentation = shapeLayer.presentation(),
let path = presentation.path {
shapeLayer.removeAnimation(forKey: "path")
shapeLayer.path = path
}
let animation = CABasicAnimation(keyPath: "path")
animation.duration = animationStyle.rawValue
animation.fromValue = shapeLayer.path
animation.timingFunction = timingFunction
layer.add(animation, forKey: "path")
}
shapeLayer.path = realPath.cgPath
}
override func layoutSubviews() {
super.layoutSubviews()
updateGraph()
}
}

View File

@@ -0,0 +1,246 @@
import UIKit
protocol ChartPreviewViewDelegate: AnyObject {
func chartPreviewView(_ view: ChartPreviewView, didChangeMinX minX: Int, maxX: Int)
}
class TintView: UIView {
let maskLayer = CAShapeLayer()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
maskLayer.fillRule = .evenOdd
layer.mask = maskLayer
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
func updateViewport(_ viewport: CGRect) {
let cornersMask = UIBezierPath(roundedRect: bounds, cornerRadius: 5)
let rectMask = UIBezierPath(rect: viewport.insetBy(dx: 11, dy: 1))
let result = UIBezierPath()
result.append(cornersMask)
result.append(rectMask)
result.usesEvenOddFillRule = true
maskLayer.path = result.cgPath
}
}
class ViewPortView: ExpandedTouchView {
let maskLayer = CAShapeLayer()
var tintView: TintView?
override init(frame: CGRect = .zero) {
super.init(frame: frame)
maskLayer.fillRule = .evenOdd
layer.mask = maskLayer
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override var frame: CGRect {
didSet {
maskLayer.path = makeMaskPath().cgPath
tintView?.updateViewport(convert(bounds, to: tintView))
}
}
func makeMaskPath() -> UIBezierPath {
let cornersMask = UIBezierPath(roundedRect: bounds, cornerRadius: 5)
let rectMask = UIBezierPath(rect: bounds.insetBy(dx: 11, dy: 1))
let result = UIBezierPath()
result.append(cornersMask)
result.append(rectMask)
result.usesEvenOddFillRule = true
return result
}
}
class ChartPreviewView: ExpandedTouchView {
let previewContainerView = UIView()
let viewPortView = ViewPortView()
let leftBoundView = UIView()
let rightBoundView = UIView()
let tintView = TintView()
var previewViews: [ChartLineView] = []
var selectorColor: UIColor = UIColor.white {
didSet {
viewPortView.backgroundColor = selectorColor
}
}
var selectorTintColor: UIColor = UIColor.clear {
didSet {
tintView.backgroundColor = selectorTintColor
}
}
var minX = 0
var maxX = 0
weak var delegate: ChartPreviewViewDelegate?
override var frame: CGRect {
didSet {
if chartData != nil {
updateViewPort()
}
}
}
var chartData: ChartPresentationData! {
didSet {
previewViews.forEach { $0.removeFromSuperview() }
previewViews.removeAll()
for i in (0..<chartData.linesCount).reversed() {
let line = chartData.lineAt(i)
let v = ChartLineView()
v.isPreview = true
v.chartLine = line
v.frame = previewContainerView.bounds
v.autoresizingMask = [.flexibleWidth, .flexibleHeight]
previewContainerView.addSubview(v)
previewViews.insert(v, at: 0)
}
previewViews.forEach { $0.setY(min: chartData.lower, max: chartData.upper) }
let count = chartData.pointsCount - 1
minX = 0
maxX = count
updateViewPort()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
previewContainerView.translatesAutoresizingMaskIntoConstraints = false
previewContainerView.layer.cornerRadius = 5
previewContainerView.clipsToBounds = true
addSubview(previewContainerView)
let t = previewContainerView.topAnchor.constraint(equalTo: topAnchor)
let b = previewContainerView.bottomAnchor.constraint(equalTo: bottomAnchor)
t.priority = .defaultHigh
b.priority = .defaultHigh
t.constant = 1
b.constant = -1
NSLayoutConstraint.activate([
previewContainerView.leftAnchor.constraint(equalTo: leftAnchor),
previewContainerView.rightAnchor.constraint(equalTo: rightAnchor),
t,
b])
tintView.frame = bounds
tintView.backgroundColor = selectorTintColor
tintView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(tintView)
viewPortView.tintView = tintView
viewPortView.backgroundColor = selectorColor
viewPortView.translatesAutoresizingMaskIntoConstraints = false
addSubview(viewPortView)
viewPortView.addSubview(leftBoundView)
viewPortView.addSubview(rightBoundView)
let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
viewPortView.addGestureRecognizer(pan)
let leftPan = UIPanGestureRecognizer(target: self, action: #selector(onLeftPan(_:)))
let rightPan = UIPanGestureRecognizer(target: self, action: #selector(onRightPan(_:)))
leftBoundView.addGestureRecognizer(leftPan)
rightBoundView.addGestureRecognizer(rightPan)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
@objc func onPan(_ sender: UIPanGestureRecognizer) {
if sender.state != .changed { return }
let p = sender.translation(in: viewPortView)
let count = chartData.labels.count - 1
let x = Int((viewPortView.frame.minX + p.x) / bounds.width * CGFloat(count))
let dx = maxX - minX
let mx = x + dx
if x > 0 && mx < count {
viewPortView.frame = viewPortView.frame.offsetBy(dx: p.x, dy: 0)
sender.setTranslation(CGPoint(x: 0, y: 0), in: viewPortView)
if x != minX {
minX = x
maxX = mx
delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX)
}
} else if minX > 0 && x <= 0 {
setX(min: 0, max: dx)
} else if maxX < count && mx >= count {
setX(min: count - dx, max: count)
}
}
@objc func onLeftPan(_ sender: UIPanGestureRecognizer) {
if sender.state != .changed { return }
let p = sender.translation(in: leftBoundView)
let count = chartData.labels.count - 1
let x = Int((viewPortView.frame.minX + p.x) / bounds.width * CGFloat(count))
if x > 0 && x < maxX && maxX - x >= count / 10 {
var f = viewPortView.frame
f = CGRect(x: f.minX + p.x, y: f.minY, width: f.width - p.x, height: f.height)
viewPortView.frame = f
rightBoundView.frame = CGRect(x: viewPortView.bounds.width - 14, y: 0, width: 44, height: viewPortView.bounds.height)
sender.setTranslation(CGPoint(x: 0, y: 0), in: leftBoundView)
if x != minX {
minX = x
delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX)
}
} else if x <= 0 && minX > 0 {
setX(min: 0, max: maxX)
}
}
@objc func onRightPan(_ sender: UIPanGestureRecognizer) {
if sender.state != .changed { return }
let p = sender.translation(in: viewPortView)
let count = chartData.labels.count - 1
let x = Int((viewPortView.frame.maxX + p.x) / bounds.width * CGFloat(count))
if x > minX && x < count && x - minX >= count / 10 {
var f = viewPortView.frame
f = CGRect(x: f.minX, y: f.minY, width: f.width + p.x, height: f.height)
viewPortView.frame = f
rightBoundView.frame = CGRect(x: viewPortView.bounds.width - 14, y: 0, width: 44, height: viewPortView.bounds.height)
sender.setTranslation(CGPoint(x: 0, y: 0), in: rightBoundView)
if x != maxX {
maxX = x
delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX)
}
} else if x >= count && maxX < count {
setX(min: minX, max: count)
}
}
func setX(min: Int, max: Int) {
assert(min < max)
minX = min
maxX = max
updateViewPort()
delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX)
}
func updateViewPort() {
let count = CGFloat(chartData.labels.count - 1)
viewPortView.frame = CGRect(x: CGFloat(minX) / count * bounds.width,
y: bounds.minY,
width: CGFloat(maxX - minX) / count * bounds.width,
height: bounds.height)
leftBoundView.frame = CGRect(x: -30, y: 0, width: 44, height: viewPortView.bounds.height)
rightBoundView.frame = CGRect(x: viewPortView.bounds.width - 14, y: 0, width: 44, height: viewPortView.bounds.height)
}
}

View File

@@ -0,0 +1,369 @@
import UIKit
enum ChartAnimation: TimeInterval {
case none = 0.0
case animated = 0.3
case interactive = 0.1
}
public class ChartView: UIView {
let chartsContainerView = ExpandedTouchView()
let chartPreviewView = ChartPreviewView()
let yAxisView = ChartYAxisView()
let xAxisView = ChartXAxisView()
let chartInfoView = ChartInfoView()
var lineViews: [ChartLineView] = []
var showPreview: Bool = false // Set true to show the preview
private var tapGR: UITapGestureRecognizer!
private var panStartPoint = 0
private var panGR: UIPanGestureRecognizer!
private var pinchStartLower = 0
private var pinchStartUpper = 0
private var pinchGR: UIPinchGestureRecognizer!
public var myPosition: Double = -1 {
didSet {
setMyPosition(myPosition)
}
}
public var previewSelectorColor: UIColor = UIColor.lightGray.withAlphaComponent(0.9) {
didSet {
chartPreviewView.selectorColor = previewSelectorColor
}
}
public var previewTintColor: UIColor = UIColor.lightGray.withAlphaComponent(0.5) {
didSet {
chartPreviewView.selectorTintColor = previewTintColor
}
}
public var infoBackgroundColor: UIColor = UIColor.white {
didSet {
chartInfoView.infoBackgroundColor = infoBackgroundColor
yAxisView.textBackgroundColor = infoBackgroundColor.withAlphaComponent(0.7)
}
}
public var infoShadowColor: UIColor = UIColor.black {
didSet {
chartInfoView.infoShadowColor = infoShadowColor
}
}
public var infoShadowOpacity: Float = 0.25 {
didSet {
chartInfoView.infoShadowOpacity = infoShadowOpacity
}
}
public var font: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) {
didSet {
xAxisView.font = font
yAxisView.font = font
chartInfoView.font = font
}
}
public var textColor: UIColor = UIColor(white: 0, alpha: 0.2) {
didSet {
xAxisView.textColor = textColor
yAxisView.textColor = textColor
chartInfoView.textColor = textColor
}
}
public var gridColor: UIColor = UIColor(white: 0, alpha: 0.2) {
didSet {
yAxisView.gridColor = gridColor
}
}
public override var backgroundColor: UIColor? {
didSet {
chartInfoView.tooltipBackgroundColor = backgroundColor ?? .white
}
}
public var chartData: ChartPresentationData! {
didSet {
lineViews.forEach { $0.removeFromSuperview() }
lineViews.removeAll()
for i in (0..<chartData.linesCount).reversed() {
let line = chartData.lineAt(i)
let v = ChartLineView()
v.clipsToBounds = true
v.chartLine = line
v.lineWidth = 3
v.frame = chartsContainerView.bounds
v.autoresizingMask = [.flexibleWidth, .flexibleHeight]
chartsContainerView.addSubview(v)
lineViews.insert(v, at: 0)
}
yAxisView.frame = chartsContainerView.bounds
yAxisView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
yAxisView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1)
chartsContainerView.addSubview(yAxisView)
chartInfoView.frame = chartsContainerView.bounds
chartInfoView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
chartInfoView.delegate = self
chartInfoView.textColor = textColor
chartsContainerView.addSubview(chartInfoView)
xAxisView.values = chartData.xAxisValues.enumerated().map { ChartXAxisView.Value(index: $0.offset, value: $0.element, text: chartData.labels[$0.offset]) }
chartPreviewView.chartData = chartData
xAxisView.setBounds(lower: chartPreviewView.minX, upper: chartPreviewView.maxX)
updateCharts()
}
}
public var isChartViewInfoHidden: Bool = false {
didSet {
chartInfoView.isHidden = isChartViewInfoHidden
chartInfoView.isUserInteractionEnabled = !isChartViewInfoHidden
}
}
public typealias OnSelectedPointChangedClosure = (_ px: CGFloat) -> Void
public var onSelectedPointChanged: OnSelectedPointChangedClosure?
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
xAxisView.font = font
xAxisView.textColor = textColor
yAxisView.font = font
yAxisView.textColor = textColor
yAxisView.gridColor = textColor
chartInfoView.font = font
chartPreviewView.selectorTintColor = previewTintColor
chartPreviewView.selectorColor = previewSelectorColor
chartInfoView.tooltipBackgroundColor = backgroundColor ?? .white
yAxisView.textBackgroundColor = infoBackgroundColor.withAlphaComponent(0.7)
tapGR = UITapGestureRecognizer(target: self, action: #selector(onTap(_:)))
chartsContainerView.addGestureRecognizer(tapGR)
panGR = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
chartsContainerView.addGestureRecognizer(panGR)
pinchGR = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(_:)))
chartsContainerView.addGestureRecognizer(pinchGR)
addSubview(chartsContainerView)
if showPreview {
addSubview(chartPreviewView)
}
chartPreviewView.delegate = self
addSubview(xAxisView)
}
public func setSelectedPoint(_ x: Double) {
let routeLength = chartData.xAxisValueAt(CGFloat(chartData.pointsCount - 1))
let upper = chartData.xAxisValueAt(CGFloat(chartPreviewView.maxX))
var lower = chartData.xAxisValueAt(CGFloat(chartPreviewView.minX))
let rangeLength = upper - lower
if x < lower || x > upper {
let current = Double(chartInfoView.infoX) * rangeLength + lower
let dx = x - current
let dIdx = Int(dx / routeLength * Double(chartData.pointsCount))
var lowerIdx = chartPreviewView.minX + dIdx
var upperIdx = chartPreviewView.maxX + dIdx
if lowerIdx < 0 {
upperIdx -= lowerIdx
lowerIdx = 0
} else if upperIdx >= chartData.pointsCount {
lowerIdx -= upperIdx - chartData.pointsCount - 1
upperIdx = chartData.pointsCount - 1
}
chartPreviewView.setX(min: lowerIdx, max: upperIdx)
lower = chartData.xAxisValueAt(CGFloat(chartPreviewView.minX))
}
chartInfoView.infoX = CGFloat((x - lower) / rangeLength)
}
fileprivate func setMyPosition(_ x: Double) {
let upper = chartData.xAxisValueAt(CGFloat(chartPreviewView.maxX))
let lower = chartData.xAxisValueAt(CGFloat(chartPreviewView.minX))
let rangeLength = upper - lower
chartInfoView.myPositionX = CGFloat((x - lower) / rangeLength)
}
override public func layoutSubviews() {
super.layoutSubviews()
let previewFrame = showPreview ? CGRect(x: bounds.minX, y: bounds.maxY - 30, width: bounds.width, height: 30) : .zero
chartPreviewView.frame = previewFrame
let xAxisFrame = CGRect(x: bounds.minX, y: bounds.maxY - previewFrame.height - 26, width: bounds.width, height: 26)
xAxisView.frame = xAxisFrame
let chartsFrame = CGRect(x: bounds.minX,
y: bounds.minY,
width: bounds.width,
height: bounds.maxY - previewFrame.height - xAxisFrame.height)
chartsContainerView.frame = chartsFrame
}
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = bounds.insetBy(dx: -30, dy: 0)
return rect.contains(point)
}
@objc func onTap(_ sender: UITapGestureRecognizer) {
guard sender.state == .ended else {
return
}
let point = sender.location(in: chartInfoView)
updateCharts(animationStyle: .none)
chartInfoView.update(point.x)
chartInfoView(chartInfoView, didMoveToPoint: point.x)
}
@objc func onPinch(_ sender: UIPinchGestureRecognizer) {
if sender.state == .began {
pinchStartLower = xAxisView.lowerBound
pinchStartUpper = xAxisView.upperBound
}
if sender.state != .changed {
return
}
let rangeLength = CGFloat(pinchStartUpper - pinchStartLower)
let dx = Int(round((rangeLength * sender.scale - rangeLength) / 2))
let lower = max(pinchStartLower + dx, 0)
let upper = min(pinchStartUpper - dx, chartData.labels.count - 1)
if upper - lower < chartData.labels.count / 10 {
return
}
chartPreviewView.setX(min: lower, max: upper)
xAxisView.setBounds(lower: lower, upper: upper)
updateCharts(animationStyle: .none)
chartInfoView.update()
}
@objc func onPan(_ sender: UIPanGestureRecognizer) {
let t = sender.translation(in: chartsContainerView)
if sender.state == .began {
panStartPoint = xAxisView.lowerBound
}
if sender.state != .changed {
return
}
let dx = Int(round(t.x / chartsContainerView.bounds.width * CGFloat(xAxisView.upperBound - xAxisView.lowerBound)))
let lower = panStartPoint - dx
let upper = lower + xAxisView.upperBound - xAxisView.lowerBound
if lower < 0 || upper > chartData.labels.count - 1 {
return
}
chartPreviewView.setX(min: lower, max: upper)
xAxisView.setBounds(lower: lower, upper: upper)
updateCharts(animationStyle: .none)
chartInfoView.update()
}
func updateCharts(animationStyle: ChartAnimation = .none) {
var lower = CGFloat(Int.max)
var upper = CGFloat(Int.min)
for i in 0..<chartData.linesCount {
let line = chartData.lineAt(i)
let subrange = line.aggregatedValues[xAxisView.lowerBound...xAxisView.upperBound]
subrange.forEach {
upper = max($0.y, upper)
if line.type == .line || line.type == .lineArea {
lower = min($0.y, lower)
}
}
}
let padding = round((upper - lower) / 10)
lower = chartData.formatter.yAxisLowerBound(from: max(0, lower - padding))
upper = chartData.formatter.yAxisUpperBound(from: upper + padding)
let steps = chartData.formatter.yAxisSteps(lowerBound: lower, upperBound: upper)
if yAxisView.upperBound != upper || yAxisView.lowerBound != lower {
yAxisView.setBounds(lower: lower,
upper: upper,
lowerLabel: chartData.formatter.yAxisString(from: Double(lower)),
upperLabel: chartData.formatter.yAxisString(from: Double(upper)),
steps: steps,
animationStyle: animationStyle)
}
lineViews.forEach {
$0.setViewport(minX: xAxisView.lowerBound,
maxX: xAxisView.upperBound,
minY: lower,
maxY: upper,
animationStyle: animationStyle)
}
}
}
extension ChartView: ChartPreviewViewDelegate {
func chartPreviewView(_ view: ChartPreviewView, didChangeMinX minX: Int, maxX: Int) {
xAxisView.setBounds(lower: minX, upper: maxX)
updateCharts(animationStyle: .none)
chartInfoView.update()
setMyPosition(myPosition)
let x = chartInfoView.infoX * CGFloat(xAxisView.upperBound - xAxisView.lowerBound) + CGFloat(xAxisView.lowerBound)
onSelectedPointChanged?(x)
}
}
extension ChartView: ChartInfoViewDelegate {
func chartInfoView(_ view: ChartInfoView, didMoveToPoint pointX: CGFloat) {
let p = convert(CGPoint(x: pointX, y: 0), from: view)
let x = (p.x / bounds.width) * CGFloat(xAxisView.upperBound - xAxisView.lowerBound) + CGFloat(xAxisView.lowerBound)
onSelectedPointChanged?(x)
}
func chartInfoView(_ view: ChartInfoView, didCaptureInfoView captured: Bool) {
panGR.isEnabled = !captured
}
func chartInfoView(_ view: ChartInfoView, infoAtPointX pointX: CGFloat) -> (String, [ChartLineInfo])? {
let p = convert(CGPoint(x: pointX, y: .zero), from: view)
let x = (p.x / bounds.width) * CGFloat(xAxisView.upperBound - xAxisView.lowerBound) + CGFloat(xAxisView.lowerBound)
let x1 = floor(x)
let x2 = ceil(x)
guard !pointX.isZero, Int(x1) < chartData.labels.count && x >= 0 else { return nil }
let label = chartData.labelAt(x)
var result: [ChartLineInfo] = []
for i in 0..<chartData.linesCount {
let line = chartData.lineAt(i)
guard line.type != .lineArea else { continue }
let y1 = line.values.altitude(at: x1 / CGFloat(chartData.pointsCount))
let y2 = line.values.altitude(at: x2 / CGFloat(chartData.pointsCount))
let dx = x - x1
let y = dx * (y2 - y1) + y1
let py = round(chartsContainerView.bounds.height * CGFloat(y - yAxisView.lowerBound) /
CGFloat(yAxisView.upperBound - yAxisView.lowerBound))
let v = round(dx * CGFloat(y2 - y1)) + CGFloat(y1)
result.append(ChartLineInfo(color: line.color,
point: chartsContainerView.convert(CGPoint(x: p.x, y: py), to: view),
formattedValue: chartData.formatter.yAxisString(from: Double(v))))
}
return (label, result)
}
}

View File

@@ -0,0 +1,130 @@
import UIKit
fileprivate class ChartXAxisInnerView: UIView {
var lowerBound = 0
var upperBound = 0
var steps: [String] = []
var labels: [UILabel] = []
var font: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) {
didSet {
labels.forEach { $0.font = font }
}
}
var textColor: UIColor = UIColor(white: 0, alpha: 0.3) {
didSet {
labels.forEach { $0.textColor = textColor }
}
}
override var frame: CGRect {
didSet {
if upperBound > 0 {
updateLabels()
}
}
}
private func makeLabel(text: String) -> UILabel {
let label = UILabel()
label.font = font
label.textColor = textColor
label.text = text
label.frame = CGRect(x: 0, y: 0, width: 60, height: 15)
return label
}
func setBounds(lower: Int, upper: Int, steps: [String]) {
lowerBound = lower
upperBound = upper
self.steps = steps
labels.forEach { $0.removeFromSuperview() }
labels.removeAll()
for i in 0..<steps.count {
let step = steps[i]
let label = makeLabel(text: step)
if i == 0 {
label.textAlignment = .left
} else if i == steps.count - 1 {
label.textAlignment = .right
} else {
label.textAlignment = .center
}
labels.append(label)
addSubview(label)
}
updateLabels()
}
private func updateLabels() {
let step = CGFloat(upperBound - lowerBound) / CGFloat(labels.count - 1)
for i in 0..<labels.count {
let x = bounds.width * step * CGFloat(i) / CGFloat(upperBound - lowerBound)
let l = labels[i]
var f = l.frame
let adjust = bounds.width > 0 ? x / bounds.width : 0
f.origin = CGPoint(x: x - f.width * adjust, y: 0)
l.frame = f
}
}
}
class ChartXAxisView: UIView {
struct Value {
let index: Int
let value: Double
let text: String
}
var lowerBound = 0
var upperBound = 0
var values: [Value] = []
var font: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) {
didSet {
labelsView?.font = font
}
}
var textColor: UIColor = UIColor(white: 0, alpha: 0.3) {
didSet {
labelsView?.textColor = textColor
}
}
private var labelsView: ChartXAxisInnerView?
func setBounds(lower: Int, upper: Int) {
lowerBound = lower
upperBound = upper
let begin = values[lower].value
let end = values[upper].value
let step = CGFloat(end - begin) / 5
var labels: [String] = []
for i in 0..<5 {
if let x = values.first(where: { $0.value >= (begin + step * CGFloat(i)) }) {
labels.append(x.text)
}
}
labels.append(values[upper].text)
let lv = ChartXAxisInnerView()
lv.frame = bounds
lv.textColor = textColor
lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(lv)
if let labelsView = labelsView {
labelsView.removeFromSuperview()
}
lv.setBounds(lower: lower, upper: upper, steps: labels)
labelsView = lv
}
}

View File

@@ -0,0 +1,252 @@
import UIKit
enum ChartYAxisViewAlignment {
case left
case right
}
fileprivate class ChartYAxisInnerView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
private static let font = UIFont.systemFont(ofSize: 12, weight: .regular)
var lowerBound: CGFloat = 0
var upperBound: CGFloat = 0
var steps: [CGFloat] = []
let lowerLabel: UILabel
let upperLabel: UILabel
let lowerLabelBackground = UIView()
let upperLabelBackground = UIView()
var alignment: ChartYAxisViewAlignment = .left
var font: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) {
didSet {
lowerLabel.font = font
upperLabel.font = font
}
}
var textColor: UIColor = UIColor(white: 0, alpha: 0.3) {
didSet {
lowerLabel.textColor = textColor
upperLabel.textColor = textColor
}
}
var textBackgroundColor: UIColor = UIColor(white: 1, alpha: 0.7) {
didSet {
lowerLabelBackground.backgroundColor = textBackgroundColor
upperLabelBackground.backgroundColor = textBackgroundColor
}
}
var gridColor: UIColor = UIColor.white {
didSet {
shapeLayer.strokeColor = gridColor.cgColor
}
}
private var path: UIBezierPath?
var shapeLayer: CAShapeLayer {
return layer as! CAShapeLayer
}
override init(frame: CGRect) {
lowerLabel = ChartYAxisInnerView.makeLabel()
upperLabel = ChartYAxisInnerView.makeLabel()
super.init(frame: frame)
lowerLabel.translatesAutoresizingMaskIntoConstraints = false
upperLabel.translatesAutoresizingMaskIntoConstraints = false
lowerLabelBackground.translatesAutoresizingMaskIntoConstraints = false
upperLabelBackground.translatesAutoresizingMaskIntoConstraints = false
lowerLabelBackground.addSubview(lowerLabel)
upperLabelBackground.addSubview(upperLabel)
addSubview(lowerLabelBackground)
addSubview(upperLabelBackground)
NSLayoutConstraint.activate([
lowerLabel.leftAnchor.constraint(equalTo: lowerLabelBackground.leftAnchor, constant: 5),
lowerLabel.topAnchor.constraint(equalTo: lowerLabelBackground.topAnchor),
lowerLabel.rightAnchor.constraint(equalTo: lowerLabelBackground.rightAnchor, constant: -5),
lowerLabel.bottomAnchor.constraint(equalTo: lowerLabelBackground.bottomAnchor),
upperLabel.leftAnchor.constraint(equalTo: upperLabelBackground.leftAnchor, constant: 5),
upperLabel.topAnchor.constraint(equalTo: upperLabelBackground.topAnchor),
upperLabel.rightAnchor.constraint(equalTo: upperLabelBackground.rightAnchor, constant: -5),
upperLabel.bottomAnchor.constraint(equalTo: upperLabelBackground.bottomAnchor),
lowerLabelBackground.topAnchor.constraint(equalTo: topAnchor, constant: 5),
lowerLabelBackground.rightAnchor.constraint(equalTo: rightAnchor, constant: -5),
upperLabelBackground.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5),
upperLabelBackground.rightAnchor.constraint(equalTo: rightAnchor, constant: -5)
])
lowerLabel.textColor = textColor
upperLabel.textColor = textColor
lowerLabelBackground.backgroundColor = textBackgroundColor
upperLabelBackground.backgroundColor = textBackgroundColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = gridColor.cgColor
shapeLayer.lineDashPattern = [2, 3]
shapeLayer.lineWidth = 1
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if upperBound > 0 && lowerBound > 0 {
updateGrid()
}
lowerLabelBackground.layer.cornerRadius = lowerLabelBackground.frame.height / 2
upperLabelBackground.layer.cornerRadius = upperLabelBackground.frame.height / 2
}
static func makeLabel() -> UILabel {
let label = UILabel()
label.font = ChartYAxisInnerView.font
label.transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1)
return label
}
func setBounds(lower: CGFloat, upper: CGFloat, lowerLabelText: String, upperLabelText: String, steps: [CGFloat]) {
lowerBound = lower
upperBound = upper
lowerLabel.text = lowerLabelText
upperLabel.text = upperLabelText
self.steps = steps
updateGrid()
}
func updateBounds(lower: CGFloat, upper: CGFloat, animationStyle: ChartAnimation = .none) {
lowerBound = lower
upperBound = upper
updateGrid(animationStyle: animationStyle)
}
func updateGrid(animationStyle: ChartAnimation = .none) {
let p = UIBezierPath()
for step in steps {
p.move(to: CGPoint(x: 0, y: step))
p.addLine(to: CGPoint(x: bounds.width, y: step))
}
let realPath = p
let yScale = (bounds.height) / CGFloat(upperBound - lowerBound)
let yTranslate = (bounds.height) * CGFloat(-lowerBound) / CGFloat(upperBound - lowerBound)
let scale = CGAffineTransform.identity.scaledBy(x: 1, y: yScale)
let translate = CGAffineTransform.identity.translatedBy(x: 0, y: yTranslate)
let transform = scale.concatenating(translate)
realPath.apply(transform)
if animationStyle != .none {
let timingFunction = CAMediaTimingFunction(name: animationStyle == .interactive ? .linear : .easeInEaseOut)
if shapeLayer.animationKeys()?.contains("path") ?? false,
let presentation = shapeLayer.presentation(),
let path = presentation.path {
shapeLayer.removeAnimation(forKey: "path")
shapeLayer.path = path
}
let animation = CABasicAnimation(keyPath: "path")
let duration = animationStyle.rawValue
animation.duration = duration
animation.fromValue = shapeLayer.path
animation.timingFunction = timingFunction
layer.add(animation, forKey: "path")
}
shapeLayer.path = realPath.cgPath
}
}
class ChartYAxisView: UIView {
var lowerBound: CGFloat = 0
var upperBound: CGFloat = 0
var alignment: ChartYAxisViewAlignment = .right
var font: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) {
didSet {
gridView?.font = font
}
}
var textColor: UIColor = UIColor(white: 0, alpha: 0.3) {
didSet {
gridView?.textColor = textColor
}
}
var textBackgroundColor: UIColor = UIColor(white: 0, alpha: 0.3) {
didSet {
gridView?.textBackgroundColor = textBackgroundColor
}
}
var gridColor: UIColor = UIColor(white: 0, alpha: 0.3) {
didSet {
gridView?.gridColor = gridColor
}
}
override var frame: CGRect {
didSet {
gridView?.updateGrid()
}
}
private var gridView: ChartYAxisInnerView?
func setBounds(lower: CGFloat,
upper: CGFloat,
lowerLabel: String,
upperLabel: String,
steps: [CGFloat],
animationStyle: ChartAnimation = .none) {
let gv = ChartYAxisInnerView()
gv.alignment = alignment
gv.textColor = textColor
gv.gridColor = gridColor
gv.textBackgroundColor = textBackgroundColor
gv.frame = bounds
gv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(gv)
if let gridView = gridView {
if animationStyle == .animated {
gv.setBounds(lower: lowerBound,
upper: upperBound,
lowerLabelText: lowerLabel,
upperLabelText: upperLabel,
steps: steps)
gv.alpha = 0
gv.updateBounds(lower: lower, upper:upper, animationStyle: animationStyle)
gridView.updateBounds(lower: lower, upper:upper, animationStyle: animationStyle)
UIView.animate(withDuration: animationStyle.rawValue, animations: {
gv.alpha = 1
gridView.alpha = 0
}) { _ in
gridView.removeFromSuperview()
}
} else {
gv.setBounds(lower: lower, upper: upper, lowerLabelText: lowerLabel, upperLabelText: upperLabel, steps: steps)
gridView.removeFromSuperview()
}
} else {
gv.setBounds(lower: lower, upper: upper, lowerLabelText: lowerLabel, upperLabelText: upperLabel, steps: steps)
}
gridView = gv
lowerBound = lower
upperBound = upper
}
}

View File

@@ -0,0 +1,8 @@
import UIKit
class ExpandedTouchView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = bounds.insetBy(dx: -30, dy: 0)
return rect.contains(point)
}
}