Commit 08487ecd by Dmitriy Stepanets

- Finished graph drawning

- Fixed ForecastTimePeriodControl for iOS 12
parent 2bfd5b55
......@@ -36,6 +36,8 @@
CD82300325D69DE400A05501 /* CityConditionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD82300225D69DE400A05501 /* CityConditionsCell.swift */; };
CD82300725D6A73F00A05501 /* CityConditionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD82300625D6A73E00A05501 /* CityConditionButton.swift */; };
CD82300A25D6B2AF00A05501 /* AppTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD82300925D6B2AF00A05501 /* AppTabBarController.swift */; };
CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */; };
CD9B6B1425DBCDE2001D9B80 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */; };
CDA69B2C2574F3C800CB6409 /* CityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA69B2B2574F3C800CB6409 /* CityCell.swift */; };
CDA69B30257500E200CB6409 /* GeoNamesPlace.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA69B2F257500E200CB6409 /* GeoNamesPlace.swift */; };
CDA69B3325750D3400CB6409 /* LocationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA69B3225750D3400CB6409 /* LocationsViewModel.swift */; };
......@@ -86,6 +88,8 @@
CD82300225D69DE400A05501 /* CityConditionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityConditionsCell.swift; sourceTree = "<group>"; };
CD82300625D6A73E00A05501 /* CityConditionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityConditionButton.swift; sourceTree = "<group>"; };
CD82300925D6B2AF00A05501 /* AppTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTabBarController.swift; sourceTree = "<group>"; };
CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CubicCurveAlgorithm.swift; sourceTree = "<group>"; };
CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = "<group>"; };
CDA69B2B2574F3C800CB6409 /* CityCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityCell.swift; sourceTree = "<group>"; };
CDA69B2F257500E200CB6409 /* GeoNamesPlace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoNamesPlace.swift; sourceTree = "<group>"; };
CDA69B3225750D3400CB6409 /* LocationsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsViewModel.swift; sourceTree = "<group>"; };
......@@ -324,6 +328,8 @@
CD1237F3255D889F00C98139 /* GradientView.swift */,
CDD0F1E72572429E00CF5017 /* AppFont.swift */,
CDD0F1ED25725BCF00CF5017 /* ThemeManager.swift */,
CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */,
CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */,
);
path = Helpers;
sourceTree = "<group>";
......@@ -483,6 +489,7 @@
CD1237F4255D889F00C98139 /* GradientView.swift in Sources */,
CD1237C3255D5C5900C98139 /* AppDelegate.swift in Sources */,
CD6B304325726AD1004B34B3 /* DefaultTheme.swift in Sources */,
CD9B6B1425DBCDE2001D9B80 /* GraphView.swift in Sources */,
870880262578F7030076BFB1 /* WeatherUpdateManager.swift in Sources */,
CDE18DD825D16CB200C80ED9 /* NavigationCityButton.swift in Sources */,
CD17C60225D15C8500EE884E /* CoordinatorProtocol.swift in Sources */,
......@@ -496,6 +503,7 @@
CD82300A25D6B2AF00A05501 /* AppTabBarController.swift in Sources */,
CD6B303E25726960004B34B3 /* ThemeProtocol.swift in Sources */,
CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */,
CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */,
CD8091772578D73F003541A4 /* PopularCitiesManager.swift in Sources */,
CDA69B2C2574F3C800CB6409 /* CityCell.swift in Sources */,
CD15DB4225DA806C00024727 /* CityForecastTimePeriodCell.swift in Sources */,
......
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "79",
"green" : "129",
"red" : "255"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "79",
"green" : "129",
"red" : "255"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "243",
"green" : "103",
"red" : "31"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "243",
"green" : "103",
"red" : "31"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
......@@ -10,6 +10,7 @@ import UIKit
class ForecastTimePeriodControl: UISegmentedControl {
private let kTopInset: CGFloat = 2
private let kSideInset:CGFloat = 11
private var selectedIndexObserver:NSKeyValueObservation?
override init(frame: CGRect) {
super.init(frame: frame)
......@@ -40,12 +41,16 @@ class ForecastTimePeriodControl: UISegmentedControl {
layer.cornerRadius = 12
//foreground
let foregroundIndex = numberOfSegments
if subviews.indices.contains(foregroundIndex),
let foregroundImageView = subviews[foregroundIndex] as? UIImageView
{
updateSegment(segmentImageView: foregroundImageView)
if #available(iOS 13, *) {
let foregroundIndex = numberOfSegments
if subviews.indices.contains(foregroundIndex),
let foregroundImageView = subviews[foregroundIndex] as? UIImageView
{
updateSegment(segmentImageView: foregroundImageView)
}
}
else {
iOS12UpdateSegments()
}
}
......@@ -77,10 +82,49 @@ class ForecastTimePeriodControl: UISegmentedControl {
UIGraphicsEndImageContext()
segmentImageView.image = image
}
private func iOS12UpdateSegments() {
let sortedSubviews = subviews.sorted{$0.frame.origin.x < $1.frame.origin.x}
for (index, subview) in sortedSubviews.enumerated() {
let segmentWidth = subview.bounds.width - 2
let segmentHeight = self.bounds.height - kTopInset * 2
subview.bounds = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
subview.layer.removeAnimation(forKey: "SelectionBounds") //this removes the weird scaling animation!
subview.layer.cornerRadius = 9
subview.clipsToBounds = false
subview.layer.shadowColor = UIColor.black.cgColor
subview.layer.shadowOpacity = 0.12
subview.layer.shadowOffset = .init(width: 0, height: 3)
subview.layer.shadowRadius = 8
//substitute with our own colored image
let imageFrame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
let rect = CGRect(origin: .zero, size: imageFrame.size)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = ThemeManager.currentTheme.segmentSelectedGradient.map{$0.cgColor}
gradientLayer.bounds = imageFrame
gradientLayer.cornerRadius = 9
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
guard let ctx = UIGraphicsGetCurrentContext() else { return }
gradientLayer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
if selectedSegmentIndex == index {
subview.backgroundColor = UIColor(patternImage: image!)
}
else {
subview.backgroundColor = nil
}
}
}
}
private extension ForecastTimePeriodControl {
func prepare() {
self.tintColor = .clear
self.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
self.backgroundColor = ThemeManager.currentTheme.segmentBackgroundColor
layer.borderColor = ThemeManager.currentTheme.segmentBorderColor.cgColor
......@@ -92,5 +136,13 @@ private extension ForecastTimePeriodControl {
self.setTitleTextAttributes([.font : AppFont.SFPro.bold(size: 14),
.foregroundColor : ThemeManager.currentTheme.segmentSelectedTextColor],
for: .selected)
self.selectedIndexObserver = self.observe(\.selectedSegmentIndex, options: .new) { (control, change) in
if #available(iOS 13, *) {
//do nothing
}
else {
self.iOS12UpdateSegments()
}
}
}
}
//
// CubicCurveAlgorithm.swift
// 1Weather
//
// Created by Dmitry Stepanets on 16.02.2021.
//
import UIKit
struct CubicCurveSegment
{
let controlPoint1: CGPoint
let controlPoint2: CGPoint
}
class CubicCurveAlgorithm
{
private var firstControlPoints: [CGPoint?] = []
private var secondControlPoints: [CGPoint?] = []
func controlPointsFromPoints(dataPoints: [CGPoint]) -> [CubicCurveSegment] {
//Number of Segments
let count = dataPoints.count - 1
//P0, P1, P2, P3 are the points for each segment, where P0 & P3 are the knots and P1, P2 are the control points.
if count == 1 {
let P0 = dataPoints[0]
let P3 = dataPoints[1]
//Calculate First Control Point
//3P1 = 2P0 + P3
let P1x = (2*P0.x + P3.x)/3
let P1y = (2*P0.y + P3.y)/3
firstControlPoints.append(CGPoint(x: P1x, y: P1y))
//Calculate second Control Point
//P2 = 2P1 - P0
let P2x = (2*P1x - P0.x)
let P2y = (2*P1y - P0.y)
secondControlPoints.append(CGPoint(x: P2x, y: P2y))
} else {
firstControlPoints = Array(repeating: nil, count: count)
var rhsArray = [CGPoint]()
//Array of Coefficients
var a = [CGFloat]()
var b = [CGFloat]()
var c = [CGFloat]()
for i in 0..<count {
var rhsValueX: CGFloat = 0
var rhsValueY: CGFloat = 0
let P0 = dataPoints[i];
let P3 = dataPoints[i+1];
if i==0 {
a.append(0)
b.append(2)
c.append(1)
//rhs for first segment
rhsValueX = P0.x + 2*P3.x;
rhsValueY = P0.y + 2*P3.y;
} else if i == count-1 {
a.append(2)
b.append(7)
c.append(0)
//rhs for last segment
rhsValueX = 8*P0.x + P3.x;
rhsValueY = 8*P0.y + P3.y;
} else {
a.append(1)
b.append(4)
c.append(1)
rhsValueX = 4*P0.x + 2*P3.x;
rhsValueY = 4*P0.y + 2*P3.y;
}
rhsArray.append(CGPoint(x: rhsValueX, y: rhsValueY))
}
//Solve Ax=B. Use Tridiagonal matrix algorithm a.k.a Thomas Algorithm
for i in 1..<count {
let rhsValueX = rhsArray[i].x
let rhsValueY = rhsArray[i].y
let prevRhsValueX = rhsArray[i-1].x
let prevRhsValueY = rhsArray[i-1].y
let m = CGFloat(a[i]/b[i-1])
let b1 = b[i] - m * c[i-1];
b[i] = b1
let r2x = rhsValueX - m * prevRhsValueX
let r2y = rhsValueY - m * prevRhsValueY
rhsArray[i] = CGPoint(x: r2x, y: r2y)
}
//Get First Control Points
//Last control Point
let lastControlPointX = rhsArray[count-1].x/b[count-1]
let lastControlPointY = rhsArray[count-1].y/b[count-1]
firstControlPoints[count-1] = CGPoint(x: lastControlPointX, y: lastControlPointY)
for i in (0 ..< count - 1).reversed() {
if let nextControlPoint = firstControlPoints[i+1] {
let controlPointX = (rhsArray[i].x - c[i] * nextControlPoint.x)/b[i]
let controlPointY = (rhsArray[i].y - c[i] * nextControlPoint.y)/b[i]
firstControlPoints[i] = CGPoint(x: controlPointX, y: controlPointY)
}
}
//Compute second Control Points from first
for i in 0..<count {
if i == count-1 {
let P3 = dataPoints[i+1]
guard let P1 = firstControlPoints[i] else{
continue
}
let controlPointX = (P3.x + P1.x)/2
let controlPointY = (P3.y + P1.y)/2
secondControlPoints.append(CGPoint(x: controlPointX, y: controlPointY))
} else {
let P3 = dataPoints[i+1]
guard let nextP1 = firstControlPoints[i+1] else {
continue
}
let controlPointX = 2*P3.x - nextP1.x
let controlPointY = 2*P3.y - nextP1.y
secondControlPoints.append(CGPoint(x: controlPointX, y: controlPointY))
}
}
}
var controlPoints = [CubicCurveSegment]()
for i in 0..<count {
if let firstControlPoint = firstControlPoints[i],
let secondControlPoint = secondControlPoints[i] {
let segment = CubicCurveSegment(controlPoint1: firstControlPoint, controlPoint2: secondControlPoint)
controlPoints.append(segment)
}
}
return controlPoints
}
}
//
// GraphView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 16.02.2021.
//
import UIKit
class GraphView: UIView {
//Private
private let kDotRadius:CGFloat = 3
private let graphColor:UIColor
private let graphTintColor:UIColor
private let lineShape = CAShapeLayer()
private let cubicCurveAlgorithm = CubicCurveAlgorithm()
init(graphColor:UIColor, tintColor:UIColor) {
self.graphColor = graphColor
self.graphTintColor = tintColor
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//Public
public func drawGraph(with points:[CGPoint]) {
//Clean up
self.layer.sublayers?.forEach {
$0.removeFromSuperlayer()
}
guard !points.isEmpty else { return }
//Add points
points.forEach {
self.addDot(point: $0)
}
//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 tintGraph(at startPoint:CGPoint, width:CGFloat) {
let tintShape = CAShapeLayer()
let path = UIBezierPath()
path.move(to: startPoint)
}
//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 {
path.addCurve(to: pointsToAdd[index],
controlPoint1: controlPoints[index - 1].controlPoint1,
controlPoint2: controlPoints[index - 1].controlPoint2)
}
return path
}
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
layer.addSublayer(dotShape)
}
}
......@@ -79,4 +79,13 @@ struct DefaultTheme: ThemeProtocol {
var segmentBackgroundColor: UIColor {
return UIColor(named: "segment_bg_color") ?? .red
}
//Graph
var graphColor: UIColor {
return UIColor(named: "graph_color") ?? .red
}
var graphTintColor: UIColor {
return UIColor(named: "graph_tint_color") ?? .red
}
}
......@@ -35,4 +35,8 @@ protocol ThemeProtocol {
var segmentBackgroundColor:UIColor { get }
var segmentBorderColor:UIColor { get }
var segmentSelectedGradient:[UIColor] { get }
//Graph
var graphColor:UIColor { get }
var graphTintColor:UIColor { get }
}
......@@ -17,9 +17,9 @@ class CityForecastTimePeriodCell: UITableViewCell {
private let summaryView = UIView()
private let summaryImageView = UIImageView()
private let summaryLabel = UILabel()
private let graphLayer = CALayer()
private let lineLayer = CAShapeLayer()
private let tempsArray = [24,25,26,25,20,21,27,24,24,20,21,25]
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]
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
......@@ -28,6 +28,7 @@ class CityForecastTimePeriodCell: UITableViewCell {
prepareSegmentedControl()
prepareScrollView()
prepareStackView()
prepareGraphView()
prepareSummaryView()
}
......@@ -44,41 +45,34 @@ class CityForecastTimePeriodCell: UITableViewCell {
guard let periodButton = stackView.arrangedSubviews.first as? PeriodForecastButton else { return }
let graphRect = periodButton.getGraphRect()
guard graphRect.width > 0 else { return }
graphView.frame = .init(x: 0, y: graphRect.origin.y, width: scrollView.contentSize.width, height: graphRect.height)
//Add graph layer if needed
if scrollView.layer.sublayers?.contains(graphLayer) == false {
graphLayer.backgroundColor = UIColor.red.cgColor
scrollView.layer.addSublayer(graphLayer)
}
graphLayer.frame = .init(x: 0, y: graphRect.origin.y, width: scrollView.contentSize.width, height: graphRect.height)
//Draw points
//Draw points and lines
self.graphView.drawGraph(with: self.getGraphPoints())
}
private func getGraphPoints() -> [CGPoint] {
var points = [CGPoint]()
let maxTemp = CGFloat(tempsArray.max() ?? 0)
let minTemp = CGFloat(tempsArray.min() ?? 0)
for index in 0..<stackView.arrangedSubviews.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 = graphRect.height / numberOfLevels
let pointLevel = ((maxTemp - CGFloat(tempsArray[index])) * levelHeight) + 5
let center = CGPoint(x: buttonCenterX,
y: pointLevel - 10)
print("Button center: \(center)")
let circleShape = CAShapeLayer()
circleShape.path = UIBezierPath(arcCenter: center,
radius: 5,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true).cgPath
circleShape.fillColor = UIColor.white.cgColor
circleShape.strokeColor = UIColor.blue.cgColor
circleShape.lineWidth = 2
graphLayer.addSublayer(circleShape)
let levelHeight = (graphView.frame.height / numberOfLevels).rounded(.down)
var pointLevel = (maxTemp - CGFloat(tempsArray[index])) * levelHeight
//Add points offset at cirle radius if needed
pointLevel = max(pointLevel, 5)
pointLevel = min(pointLevel, graphView.frame.height - 5)
points.append(.init(x: buttonCenterX, y: pointLevel))
}
return points
}
}
......@@ -118,6 +112,8 @@ private extension CityForecastTimePeriodCell {
stackView.alignment = .center
stackView.spacing = 10
stackView.clipsToBounds = false
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = .init(top: 0, left: 6, bottom: 0, right: 6)
scrollView.addSubview(stackView)
stackView.snp.makeConstraints { (make) in
......@@ -130,6 +126,13 @@ private extension CityForecastTimePeriodCell {
}
}
func prepareGraphView() {
//Graph view
graphView.frame = .zero
graphView.backgroundColor = .clear
scrollView.addSubview(graphView)
}
func prepareSummaryView() {
summaryImageView.contentMode = .scaleAspectFit
summaryImageView.image = UIImage(named: "hot_indicator")
......
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