forked from shadowfacts/Tusker
150 lines
5.1 KiB
Swift
150 lines
5.1 KiB
Swift
//
|
|
// TrendHistoryView.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 1/24/21.
|
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
|
|
class TrendHistoryView: UIView {
|
|
|
|
private var history: [History]?
|
|
|
|
private let curveRadius: CGFloat = 10
|
|
|
|
/// The base background color used for the graph fill.
|
|
var effectiveBackgroundColor = UIColor.appBackground
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
createLayers()
|
|
}
|
|
|
|
// Unneeded on visionOS, since there is no dark/light mode
|
|
#if !os(visionOS)
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
createLayers()
|
|
}
|
|
#endif
|
|
|
|
func setHistory(_ history: [History]?) {
|
|
if let history = history {
|
|
self.history = history.sorted(by: { $0.day < $1.day })
|
|
} else {
|
|
self.history = nil
|
|
}
|
|
|
|
createLayers()
|
|
}
|
|
|
|
private func createLayers() {
|
|
guard let history = history,
|
|
history.count >= 2,
|
|
!bounds.isEmpty else {
|
|
return
|
|
}
|
|
|
|
let maxUses = history.max(by: { $0.uses < $1.uses })!.uses
|
|
guard maxUses > 0 else {
|
|
return
|
|
}
|
|
|
|
// remove old layers if this view is being re-used
|
|
layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
|
|
|
let path = UIBezierPath()
|
|
|
|
let widthStep = bounds.width / CGFloat(history.count - 1)
|
|
|
|
let points: [CGPoint] = history.enumerated().map { (index, entry) in
|
|
let x = CGFloat(index) * widthStep
|
|
let yFrac = CGFloat(entry.uses) / CGFloat(maxUses)
|
|
let y = (1 - yFrac) * bounds.height
|
|
return CGPoint(x: x, y: y)
|
|
}
|
|
|
|
var gapStartPoints = [CGPoint]()
|
|
var gapEndPoints = [CGPoint]()
|
|
|
|
for (index, point) in points.enumerated().dropFirst().dropLast() {
|
|
let prev = points[index - 1]
|
|
let next = points[index + 1]
|
|
|
|
let a = atan((point.y - prev.y) / widthStep)
|
|
let b = atan((next.y - point.y) / widthStep)
|
|
let innerAngle = .pi - a - b
|
|
|
|
let gapDistance = curveRadius / sin(innerAngle / 2)
|
|
|
|
let x1 = point.x - cos(a) * gapDistance
|
|
let y1 = point.y - sin(a) * gapDistance
|
|
gapStartPoints.append(CGPoint(x: x1, y: y1))
|
|
|
|
let x2 = point.x + cos(b) * gapDistance
|
|
let y2 = point.y + sin(b) * gapDistance
|
|
gapEndPoints.append(CGPoint(x: x2, y: y2))
|
|
}
|
|
|
|
path.move(to: points.first!)
|
|
for (index, point) in points.dropFirst().dropLast().enumerated() {
|
|
path.addLine(to: gapStartPoints[index])
|
|
path.addQuadCurve(to: gapEndPoints[index], controlPoint: point)
|
|
}
|
|
path.addLine(to: points.last!)
|
|
|
|
let borderLayer = CAShapeLayer()
|
|
// copy the border path so we can continue mutating the UIBezierPath to create the fill path
|
|
borderLayer.path = path.cgPath.copy()!
|
|
borderLayer.strokeColor = tintColor.cgColor
|
|
borderLayer.fillColor = nil
|
|
borderLayer.lineWidth = lineWidth
|
|
borderLayer.lineCap = .round
|
|
|
|
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
|
|
path.addLine(to: CGPoint(x: 0, y: bounds.height))
|
|
path.addLine(to: points.first!)
|
|
|
|
let fillLayer = CAShapeLayer()
|
|
fillLayer.path = path.cgPath
|
|
let fillColor = self.fillColor()
|
|
fillLayer.strokeColor = fillColor
|
|
fillLayer.fillColor = fillColor
|
|
fillLayer.lineWidth = lineWidth
|
|
|
|
layer.addSublayer(fillLayer)
|
|
layer.addSublayer(borderLayer)
|
|
}
|
|
|
|
private var lineWidth: CGFloat {
|
|
(traitCollection.preferredContentSizeCategory > .large || UIAccessibility.isBoldTextEnabled) ? 4 : 2
|
|
}
|
|
|
|
// The non-transparent fill color.
|
|
// We blend with the view's background color ourselves so that final color is non-transparent,
|
|
// otherwise when the fill layer's border and fill overlap, there's a visibly darker patch
|
|
// because transparent colors are being blended together.
|
|
private func fillColor() -> CGColor {
|
|
var backgroundRed: CGFloat = 0
|
|
var backgroundGreen: CGFloat = 0
|
|
var backgroundBlue: CGFloat = 0
|
|
var tintRed: CGFloat = 0
|
|
var tintGreen: CGFloat = 0
|
|
var tintBlue: CGFloat = 0
|
|
traitCollection.performAsCurrent {
|
|
effectiveBackgroundColor.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil)
|
|
tintColor.getRed(&tintRed, green: &tintGreen, blue: &tintBlue, alpha: nil)
|
|
}
|
|
let blendedRed = (backgroundRed + tintRed) / 2
|
|
let blendedGreen = (backgroundGreen + tintGreen) / 2
|
|
let blendedBlue = (backgroundBlue + tintBlue) / 2
|
|
return CGColor(red: blendedRed, green: blendedGreen, blue: blendedBlue, alpha: 1)
|
|
}
|
|
|
|
}
|