Commit 4782e52f by Dmitriy Stepanets

Working on additional graphic

parent b8dd6f8e
......@@ -17,23 +17,22 @@ struct GraphLine {
//Private
private static let cubicCurveAlgorithm = CubicCurveAlgorithm()
private let kIntersectAccuracy:CGFloat = 2
private let points:[CGPoint]
private let tintColor:UIColor
private let graphViewRect:CGRect
private var points = [CGPoint]()
private let settings:GraphLineSettings
private let onGetGraphRect: GraphRectClosure
private var sections = [CubicCurve]()
//Public
let lineShape = CAShapeLayer()
private(set) var tintLineShape = CAShapeLayer()
private(set) var lineDots = [LineDot]()
public typealias GraphRectClosure = () -> CGRect
public private(set) var lineShape = CAShapeLayer()
public private(set) var tintLineShape = CAShapeLayer()
public private(set) var lineDots = [LineDot]()
init(graphViewRect:CGRect, points:[CGPoint], settings:GraphLineSettings) {
self.graphViewRect = graphViewRect
self.points = points
self.tintColor = settings.tintColor
init(settings:GraphLineSettings, onGetGraphRect:@escaping GraphRectClosure) {
self.onGetGraphRect = onGetGraphRect
self.settings = settings
//Line
lineShape.path = self.linePath(from: points).cgPath
lineShape.fillColor = UIColor.clear.cgColor
lineShape.strokeColor = settings.color.cgColor
lineShape.lineWidth = settings.lineWidth
......@@ -53,26 +52,12 @@ struct GraphLine {
tintLineShape.lineCap = .round
tintLineShape.lineJoin = .round
tintLineShape.path = nil
//Dots
self.points.forEach {
let dotShape = CAShapeLayer()
dotShape.path = UIBezierPath(arcCenter: $0,
radius: settings.dotRadius,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true).cgPath
dotShape.fillColor = UIColor.white.cgColor
dotShape.strokeColor = settings.color.cgColor
dotShape.lineWidth = settings.dotLineWidth
lineDots.append(.init(center: $0, shape: dotShape))
}
}
private mutating func linePath(from points:[CGPoint]) -> UIBezierPath {
let path = UIBezierPath()
let startPoint = CGPoint(x: 0, y: self.graphViewRect.height)
let endPoint = CGPoint(x: self.graphViewRect.width, y: self.graphViewRect.height)
let startPoint = CGPoint(x: 0, y: self.onGetGraphRect().height)
let endPoint = CGPoint(x: self.onGetGraphRect().width, y: self.onGetGraphRect().height)
var pointsToAdd = [CGPoint]()
pointsToAdd.append(startPoint)
......@@ -119,9 +104,9 @@ struct GraphLine {
}
let leftLine = LineSegment(p0: .init(x: startPointX + kIntersectAccuracy, y: 0),
p1: .init(x: startPointX + kIntersectAccuracy, y: self.graphViewRect.height))
p1: .init(x: startPointX + kIntersectAccuracy, y: self.onGetGraphRect().height))
let rightLine = LineSegment(p0: .init(x: endPointX - kIntersectAccuracy, y: 0),
p1: .init(x: endPointX - kIntersectAccuracy, y: self.graphViewRect.height))
p1: .init(x: endPointX - kIntersectAccuracy, y: self.onGetGraphRect().height))
//Get all sections for the given tint and cut them
let tintPath = UIBezierPath()
......@@ -131,9 +116,9 @@ struct GraphLine {
let minRightPointX = min(section.endingPoint.x, rightLine.endingPoint.x)
let leftBoundary = LineSegment(p0: .init(x: maxLeftPointX, y: 0),
p1: .init(x: maxLeftPointX, y: self.graphViewRect.height))
p1: .init(x: maxLeftPointX, y: self.onGetGraphRect().height))
let rightBoundary = LineSegment(p0: .init(x: minRightPointX, y: 0),
p1: .init(x: minRightPointX, y: self.graphViewRect.height))
p1: .init(x: minRightPointX, y: self.onGetGraphRect().height))
if let subcurve = getSubcurvePath(baseCurve: section,
leftBoundary: leftBoundary,
rightBoundary: rightBoundary)
......@@ -157,4 +142,33 @@ struct GraphLine {
guard let dotToTint = (lineDots.first{$0.center == point}) else { return }
dotToTint.shape.strokeColor = ThemeManager.currentTheme.graphTintColor.cgColor
}
mutating func updateWith(points:[CGPoint]) {
self.lineDots.forEach {
$0.shape.removeFromSuperlayer()
}
self.sections.removeAll()
self.lineDots.removeAll()
self.points = points
guard !self.points.isEmpty else { return }
//Add new dots
self.points.forEach {
let dotShape = CAShapeLayer()
dotShape.path = UIBezierPath(arcCenter: $0,
radius: settings.dotRadius,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true).cgPath
dotShape.fillColor = UIColor.white.cgColor
dotShape.strokeColor = settings.color.cgColor
dotShape.lineWidth = settings.dotLineWidth
lineDots.append(.init(center: $0, shape: dotShape))
}
//Refresh line path
lineShape.path = self.linePath(from: points).cgPath
}
}
......@@ -10,22 +10,43 @@ import BezierKit
class GraphView: UIView {
//Private
private let kIntersectAccuracy:CGFloat = 2
private let kDotRadius:CGFloat = 3
private let graphColor:UIColor
private let graphTintColor:UIColor
private let lineShape = CAShapeLayer()
private let tintShape = CAShapeLayer()
private var lineDots = [LineDot]()
private let cubicCurveAlgorithm = CubicCurveAlgorithm()
private var sections = [CubicCurve]()
private let mainLineSettings = GraphLineSettings(lineWidth: 3,
dotRadius: 3,
dotLineWidth: 2,
color: ThemeManager.currentTheme.graphColor,
tintColor: ThemeManager.currentTheme.graphTintColor)
private let additionalLineSettings = GraphLineSettings(lineWidth: 2,
dotRadius: 2,
dotLineWidth: 1 / UIScreen.main.scale,
color: UIColor(hex: 0xa4a4a4).withAlphaComponent(0.7),
tintColor: UIColor(hex: 0x434343).withAlphaComponent(0.7))
private lazy var mainLine:GraphLine = {
let line = GraphLine(settings: mainLineSettings, onGetGraphRect: {
return self.frame
})
return line
}()
private lazy var additionalLine:GraphLine = {
let line = GraphLine(settings: additionalLineSettings, onGetGraphRect: {
return self.frame
})
return line
}()
//MARK:- View life cycle
init(graphColor:UIColor, tintColor:UIColor) {
self.graphColor = graphColor
self.graphTintColor = tintColor
init() {
super.init(frame: .zero)
self.isUserInteractionEnabled = false
//Adding lines
layer.insertSublayer(mainLine.lineShape, at: 0)
layer.insertSublayer(mainLine.tintLineShape, at: 1)
layer.insertSublayer(additionalLine.lineShape, at: 0)
layer.insertSublayer(additionalLine.tintLineShape, at: 0)
}
required init?(coder: NSCoder) {
......@@ -33,147 +54,30 @@ class GraphView: UIView {
}
//Public
public func drawGraph(with points:[CGPoint]) {
//Clean up
self.layer.sublayers?.forEach {
$0.removeFromSuperlayer()
}
sections.removeAll()
lineDots.removeAll()
guard !points.isEmpty else { return }
//Add points
points.forEach {
self.addDot(point: $0)
public func drawMainGraph(with points:[CGPoint]) {
mainLine.updateWith(points: points)
mainLine.lineDots.forEach {
layer.addSublayer($0.shape)
}
//Add line
lineShape.frame = self.bounds
lineShape.path = self.linePath(from: points).cgPath
lineShape.fillColor = UIColor.clear.cgColor
lineShape.strokeColor = graphColor.cgColor
lineShape.lineWidth = 3
lineShape.lineCap = .round
lineShape.lineJoin = .round
//Shadow
lineShape.shadowColor = UIColor.black.cgColor
lineShape.shadowOffset = .init(width: 0, height: 6)
lineShape.shadowRadius = 3
lineShape.shadowOpacity = 0.1
layer.insertSublayer(lineShape, at: 0)
}
public func tintGraphFrom(startPointX:CGFloat, endPointX:CGFloat) {
func getSubcurvePath(baseCurve:CubicCurve, leftBoundary:LineSegment, rightBoundary:LineSegment) -> UIBezierPath? {
guard
let leftIntersection = baseCurve.intersections(with: leftBoundary).first,
let rightIntersection = baseCurve.intersections(with: rightBoundary).first
else {
return nil
}
let subcurve = baseCurve.split(from: leftIntersection.t1, to: rightIntersection.t1)
let path = UIBezierPath()
path.move(to: subcurve.startingPoint)
path.addCurve(to: subcurve.endingPoint, controlPoint1: subcurve.p1, controlPoint2: subcurve.p2)
return path
}
let leftLine = LineSegment(p0: .init(x: startPointX + kIntersectAccuracy, y: 0),
p1: .init(x: startPointX + kIntersectAccuracy, y: self.bounds.height))
let rightLine = LineSegment(p0: .init(x: endPointX - kIntersectAccuracy, y: 0),
p1: .init(x: endPointX - kIntersectAccuracy, y: self.bounds.height))
//Get all sections for the given tint and cut them
let tintPath = UIBezierPath()
for section in sections {
if section.startingPoint.x >= leftLine.p0.x || section.endingPoint.x <= rightLine.p0.x {
let maxLeftPointX = max(section.startingPoint.x, leftLine.startingPoint.x)
let minRightPointX = min(section.endingPoint.x, rightLine.endingPoint.x)
let leftBoundary = LineSegment(p0: .init(x: maxLeftPointX, y: 0),
p1: .init(x: maxLeftPointX, y: self.bounds.height))
let rightBoundary = LineSegment(p0: .init(x: minRightPointX, y: 0),
p1: .init(x: minRightPointX, y: self.bounds.height))
if let subcurve = getSubcurvePath(baseCurve: section,
leftBoundary: leftBoundary,
rightBoundary: rightBoundary)
{
tintPath.append(subcurve)
}
}
}
//Check for empty path
if tintPath.isEmpty { return }
tintShape.path = tintPath.cgPath
if tintShape.superlayer == nil {
tintShape.fillColor = UIColor.clear.cgColor
tintShape.strokeColor = ThemeManager.currentTheme.graphTintColor.cgColor
tintShape.lineWidth = 3
tintShape.lineCap = .round
tintShape.lineJoin = .round
layer.insertSublayer(tintShape, at: 1)
public func drawAdditionalGraph(with points:[CGPoint]) {
additionalLine.updateWith(points: points)
additionalLine.lineDots.forEach {
layer.addSublayer($0.shape)
}
}
public func tintDot(at: CGPoint) {
lineDots.forEach {
$0.shape.strokeColor = ThemeManager.currentTheme.graphColor.cgColor
}
guard let dotToTint = (lineDots.first{$0.center == at}) else { return }
dotToTint.shape.strokeColor = ThemeManager.currentTheme.graphTintColor.cgColor
public func tintGraphFrom(startPointX:CGFloat, endPointX:CGFloat) {
mainLine.tintLineFrom(startPointX: startPointX, to: endPointX)
additionalLine.tintLineFrom(startPointX: startPointX, to: endPointX)
}
//Private
private func linePath(from points:[CGPoint]) -> UIBezierPath {
let path = UIBezierPath()
let startPoint = CGPoint(x: 0, y: self.frame.height)
let endPoint = CGPoint(x: self.frame.width, y: self.frame.height)
var pointsToAdd = [CGPoint]()
pointsToAdd.append(startPoint)
pointsToAdd.append(contentsOf: points)
pointsToAdd.append(endPoint)
path.move(to: pointsToAdd.first!)
if pointsToAdd.count == 2 {
path.addLine(to: points[1])
return path
}
let controlPoints = cubicCurveAlgorithm.controlPointsFromPoints(dataPoints: pointsToAdd)
for index in 1..<pointsToAdd.count {
sections.append(.init(p0: pointsToAdd[index - 1],
p1: controlPoints[index - 1].controlPoint1,
p2: controlPoints[index - 1].controlPoint2,
p3: pointsToAdd[index]))
path.addCurve(to: pointsToAdd[index],
controlPoint1: controlPoints[index - 1].controlPoint1,
controlPoint2: controlPoints[index - 1].controlPoint2)
}
return path
public func tintMainDotAt(point: CGPoint) {
mainLine.tintDotAt(point: point)
}
private func addDot(point:CGPoint) {
let dotShape = CAShapeLayer()
dotShape.path = UIBezierPath(arcCenter: point,
radius: kDotRadius,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true).cgPath
dotShape.fillColor = UIColor.white.cgColor
dotShape.strokeColor = self.graphColor.cgColor
dotShape.lineWidth = 2
lineDots.append(.init(center: point, shape: dotShape))
layer.addSublayer(dotShape)
public func tintAdditionalDotAt(point: CGPoint) {
additionalLine.tintDotAt(point: point)
}
}
......@@ -7,22 +7,34 @@
import UIKit
struct DayTemp {
let min:CGFloat
let max:CGFloat
}
class CityForecastTimePeriodCell: UITableViewCell {
static let kIdentifier = "cityForecastTimePeriodCell"
//Private
private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
private let kMinGraphHeight:CGFloat = 50
private let kMinGraphHeight:CGFloat = 20
private let scrollView = UIScrollView()
private let stackView = UIStackView()
private let summaryView = UIView()
private let summaryImageView = UIImageView()
private let summaryLabel = UILabel()
private var graphPoints = [CGPoint]()
private let graphView = GraphView(graphColor: ThemeManager.currentTheme.graphColor,
tintColor: ThemeManager.currentTheme.graphTintColor)
private let tempsArray = [20,18,25,24,22,20,23,25,27,26,23,20]
private var maxTempGraphPoints = [CGPoint]()
private var minTempGraphPoints = [CGPoint]()
private let graphView = GraphView()
private let dayTemps = [DayTemp(min: 8, max: 17),
DayTemp(min: 9, max: 17),
DayTemp(min: 7, max: 18),
DayTemp(min: 7, max: 17),
DayTemp(min: 8, max: 17),
DayTemp(min: 8, max: 18),
DayTemp(min: 9, max: 18),
DayTemp(min: 8, max: 16)]
private var graphIsDrawn = false
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
......@@ -57,7 +69,7 @@ class CityForecastTimePeriodCell: UITableViewCell {
width: scrollView.contentSize.width,
height: periodButton.graphRect.height)
graphPoints = getGraphPoints()
updateGraphPoints()
drawGraph()
graphIsDrawn = true
}
......@@ -71,7 +83,8 @@ class CityForecastTimePeriodCell: UITableViewCell {
return
}
self.graphView.drawGraph(with: graphPoints)
self.graphView.drawMainGraph(with: maxTempGraphPoints)
self.graphView.drawAdditionalGraph(with: minTempGraphPoints)
self.tintGraphAt(button: selectedButton)
print("Draw graph!")
}
......@@ -79,31 +92,39 @@ class CityForecastTimePeriodCell: UITableViewCell {
private func tintGraphAt(button:PeriodForecastButton) {
self.graphView.tintGraphFrom(startPointX: button.frame.origin.x,
endPointX: button.frame.origin.x + button.bounds.width)
self.graphView.tintDot(at: graphPoints[button.index])
self.graphView.tintMainDotAt(point: maxTempGraphPoints[button.index])
self.graphView.tintAdditionalDotAt(point: minTempGraphPoints[button.index])
}
private func getGraphPoints() -> [CGPoint] {
var points = [CGPoint]()
let maxTemp = CGFloat(tempsArray.max() ?? 0)
let minTemp = CGFloat(tempsArray.min() ?? 0)
private func updateGraphPoints(){
self.maxTempGraphPoints.removeAll()
self.minTempGraphPoints.removeAll()
for index in 0..<stackView.arrangedSubviews.count {
let maxTemp = CGFloat(dayTemps.map{$0.max}.max() ?? 0)
let minTemp = CGFloat(dayTemps.map{$0.min}.min() ?? 0)
for index in 0..<dayTemps.count {
guard let stackButton = stackView.arrangedSubviews[index] as? PeriodForecastButton else { continue }
let buttonRightSide = stackButton.frame.origin.x + stackButton.bounds.width
let buttonCenterX = (buttonRightSide + stackButton.frame.origin.x) / 2
let numberOfLevels = maxTemp - minTemp
let levelHeight = (graphView.frame.height / numberOfLevels).rounded(.down)
var pointLevel = (maxTemp - CGFloat(tempsArray[index])) * levelHeight
var maxPointLevel = (maxTemp - dayTemps[index].max) * levelHeight
var minPointLevel = (minTemp + dayTemps[index].min) * levelHeight
//Add points offset at cirle radius if needed
pointLevel = max(pointLevel, 5)
pointLevel = min(pointLevel, graphView.frame.height - 5)
maxPointLevel = max(maxPointLevel, 5)
maxPointLevel = min(maxPointLevel, graphView.frame.height - 5)
minPointLevel = min(minPointLevel, 5)
minPointLevel = max(minPointLevel, graphView.frame.height - 5)
points.append(.init(x: buttonCenterX, y: pointLevel))
maxTempGraphPoints.append(.init(x: buttonCenterX, y: maxPointLevel))
minTempGraphPoints.append(.init(x: buttonCenterX, y: minPointLevel))
}
return points
print("break")
}
@objc private func handleConditionButton(button: PeriodForecastButton) {
......@@ -164,8 +185,9 @@ private extension CityForecastTimePeriodCell {
make.edges.height.equalToSuperview()
}
for index in 0..<12 {
for index in 0..<dayTemps.count {
let conditionButton = PeriodForecastButton()
conditionButton.configure(dayTemp: dayTemps[index])
conditionButton.index = index
conditionButton.addTarget(self, action: #selector(handleConditionButton(button:)), for: .touchUpInside)
conditionButton.isSelected = index == 1
......
......@@ -13,6 +13,7 @@ class PeriodForecastButton: UIControl {
private let forecastImageView = UIImageView()
private let tempLabel = UILabel()
private let indicatorImageView = UIImageView()
private let minTempLabel = UILabel()
private let timeLabel = UILabel()
//Public
......@@ -23,7 +24,7 @@ class PeriodForecastButton: UIControl {
return .init(x: 0,
y: topInset,
width: self.bounds.width,
height: self.indicatorImageView.frame.origin.y - topInset - kGraphInset)
height: self.minTempLabel.frame.origin.y - topInset - kGraphInset)
}
override init(frame: CGRect) {
......@@ -32,6 +33,7 @@ class PeriodForecastButton: UIControl {
prepareButton()
prepareForecastImage()
prepareTempLabel()
prepareMinTempLabel()
preparePeriodIndicator()
prepareTimeLabel()
}
......@@ -45,6 +47,7 @@ class PeriodForecastButton: UIControl {
if isSelected {
self.backgroundColor = UIColor.white
self.tempLabel.font = AppFont.SFPro.bold(size: 16)
self.minTempLabel.font = AppFont.SFPro.bold(size: 12)
self.timeLabel.font = AppFont.SFPro.bold(size: 12)
self.indicatorImageView.alpha = 1.0
self.layer.shadowColor = UIColor(hex: 0xe5e6f4).cgColor
......@@ -55,6 +58,7 @@ class PeriodForecastButton: UIControl {
else {
self.backgroundColor = UIColor.white.withAlphaComponent(0.5)
self.tempLabel.font = AppFont.SFPro.regular(size: 16)
self.minTempLabel.font = AppFont.SFPro.regular(size: 12)
self.timeLabel.font = AppFont.SFPro.regular(size: 12)
self.indicatorImageView.alpha = 0.5
self.layer.shadowColor = UIColor.clear.cgColor
......@@ -64,6 +68,12 @@ class PeriodForecastButton: UIControl {
}
}
}
//Public
public func configure(dayTemp:DayTemp) {
self.tempLabel.text = "\(Int(dayTemp.max))°"
self.minTempLabel.text = "\(Int(dayTemp.min))°"
}
}
private extension PeriodForecastButton {
......@@ -101,6 +111,18 @@ private extension PeriodForecastButton {
}
}
func prepareMinTempLabel() {
minTempLabel.isUserInteractionEnabled = false
minTempLabel.font = AppFont.SFPro.regular(size: 12)
minTempLabel.textColor = ThemeManager.currentTheme.secondaryTextColor
addSubview(minTempLabel)
minTempLabel.snp.makeConstraints { (make) in
make.centerX.equalToSuperview()
make.top.greaterThanOrEqualTo(tempLabel.snp.bottom).offset(55)
}
}
func preparePeriodIndicator() {
indicatorImageView.isUserInteractionEnabled = false
indicatorImageView.contentMode = .scaleAspectFit
......@@ -110,7 +132,7 @@ private extension PeriodForecastButton {
indicatorImageView.snp.makeConstraints { (make) in
make.width.height.equalTo(12)
make.centerX.equalToSuperview()
make.top.greaterThanOrEqualTo(tempLabel.snp.bottom).offset(100)
make.top.equalTo(minTempLabel.snp.bottom).offset(10)
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment