Created
August 13, 2018 22:27
-
-
Save tobins/4c30016b71385f6094698e960fea50e7 to your computer and use it in GitHub Desktop.
CAShapeLayer as a Graphy (playgrounds)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: A UIKit based Playground for presenting user interface | |
import UIKit | |
import PlaygroundSupport | |
protocol GraphViewDataSource { | |
func numberOfGraphs(in graphView:GraphView) -> Int | |
func graphView(_ graphView:GraphView, numberOfValuesForGraph graph:Int) -> Int | |
func graphView(_ graphView:GraphView, valueForGraphAt indexPath:IndexPath) -> Float | |
} | |
protocol GraphViewDelegate { | |
func graphView(_ graphView:GraphView, colorForGraph graph:Int) -> UIColor | |
func graphView(_ graphView:GraphView, lineWidthForGraph graph:Int) -> CGFloat | |
func graphView(_ graphView:GraphView, fillStyleForGraph graph:Int) -> GraphFillStyle? | |
func graphView(_ graphView:GraphView, strokeStyleForGraph graph:Int) -> GraphStrokeStyle? | |
} | |
extension GraphViewDataSource { | |
func numberOfGraphs(in graphView:GraphView) -> Int { | |
return 0 | |
} | |
func graphView(_ graphView:GraphView, numberOfValuesOnGraph graph:Int) -> Int { | |
return 0 | |
} | |
func graphView(_ graphView:GraphView, valueForGraphAt indexPath:IndexPath) -> Float { | |
return Float.nan | |
} | |
} | |
extension GraphViewDelegate { | |
func graphView(_ graphView:GraphView, colorForGraph graph:Int) -> UIColor { | |
return UIColor.white | |
} | |
func graphView(_ graphView:GraphView, lineWidthForGraph graph:Int) -> CGFloat { | |
return 1.0 | |
} | |
func graphView(_ graphView:GraphView, fillStyleForGraph graph:Int) -> GraphFillStyle? { | |
return nil | |
} | |
func graphView(_ graphView:GraphView, strokeStyleForGraph graph:Int) -> GraphStrokeStyle? { | |
return nil | |
} | |
} | |
class GraphFillStyle { | |
var startPoint:CGPoint = CGPoint(x:0, y:0) | |
var endPoint:CGPoint = CGPoint(x:0, y: 1) | |
var colors:[UIColor] = [] | |
var opacity:Float = 1.0 | |
var fillColor:UIColor = .black | |
} | |
class GraphStrokeStyle { | |
var shadowColor:UIColor = .black | |
var shadowRadius:CGFloat = 4 | |
var shadowOpacity:Float = 0.3 | |
var shadowOffset:CGSize = CGSize(width: 0, height: 2) | |
var lineCap:String = kCALineCapRound | |
} | |
class GraphView:UIView { | |
var graphLayer:CALayer? = nil | |
var axisShape:CAShapeLayer? = nil | |
var dataSource:GraphViewDataSource? = nil | |
var delegate:GraphViewDelegate? = nil | |
var axisColor:UIColor = UIColor(white: 1.0, alpha: 0.05) | |
var axisLineWidth:CGFloat = 1.0 | |
var horizontalAxisSegments:Int = 0 | |
var verticalAxisSegments:Int = 0 | |
private func points(from values:[Float], padding:CGFloat = 0, offset:CGFloat = 0) -> [CGPoint]? { | |
guard values.count != 0 else { | |
return nil | |
} | |
let segmentWidth = self.frame.width / CGFloat(values.count-1) | |
var points:[CGPoint] = [] | |
var min = values.first! | |
var max = values.first! | |
for value in values { | |
if max < value { | |
max = value | |
} | |
if min > value { | |
min = value | |
} | |
} | |
let diff = max - min | |
for (index, value) in values.enumerated() { | |
let x = segmentWidth * CGFloat(index) | |
let y = (self.frame.height-padding) * CGFloat((value-min)/diff)+offset | |
points.append( CGPoint(x: x, y: y)) | |
} | |
return points | |
} | |
func axis(withX x:Int, Y y:Int) -> CAShapeLayer { | |
let shape = CAShapeLayer() | |
let path = UIBezierPath() | |
for i in 0..<x{ | |
path.move(to: CGPoint(x: CGFloat(i) * self.frame.width / CGFloat(x), y: 0) ) | |
path.addLine(to: CGPoint(x: CGFloat(i) * self.frame.width / CGFloat(x), y: self.frame.height) ) | |
} | |
for i in 0..<y{ | |
path.move(to: CGPoint(x: 0, y: CGFloat(i) * self.frame.height / CGFloat(y) )) | |
path.addLine(to: CGPoint(x: self.frame.width, y: CGFloat(i) * self.frame.height / CGFloat(y) )) | |
} | |
shape.path = path.cgPath | |
shape.lineWidth = self.axisLineWidth | |
shape.strokeColor = self.axisColor.cgColor | |
shape.fillColor = UIColor.clear.cgColor | |
return shape | |
} | |
func line(from values:[Float], color:UIColor, lineWidth:CGFloat, style:GraphStrokeStyle? = nil ) -> CAShapeLayer? { | |
guard let points = self.points(from: values, padding: lineWidth * 2, offset: lineWidth) else { | |
return nil | |
} | |
let shape = CAShapeLayer() | |
let path = UIBezierPath() | |
if let point = points.first { | |
path.move(to: point) | |
} | |
for (index, point) in points[1..<points.count].enumerated() { | |
let previous = points[index] | |
let dX = (point.x - previous.x) / 2 | |
let p1 = CGPoint(x: previous.x + dX, y: previous.y) | |
let p2 = CGPoint(x: point.x - dX, y: point.y) | |
path.addCurve(to: point, controlPoint1: p1, controlPoint2: p2) | |
} | |
shape.path = path.cgPath | |
shape.lineWidth = lineWidth | |
if let style = style { | |
shape.shadowColor = style.shadowColor.cgColor | |
shape.shadowRadius = style.shadowRadius | |
shape.shadowOpacity = style.shadowOpacity | |
shape.shadowOffset = style.shadowOffset | |
shape.lineCap = style.lineCap | |
} | |
shape.strokeColor = color.cgColor | |
shape.fillColor = UIColor.clear.cgColor | |
return shape | |
} | |
func fill(from values:[Float], style:GraphFillStyle? = nil) -> CAShapeLayer? { | |
guard let points = self.points(from:values) else { | |
return nil | |
} | |
let shape = CAShapeLayer() | |
let path = UIBezierPath() | |
let first = points.first! | |
let last = points.last! | |
path.move(to: first) | |
for (index, point) in points[1..<points.count].enumerated() { | |
let previous = points[index] | |
let dX = (point.x - previous.x) / 2 | |
let p1 = CGPoint(x: previous.x + dX, y: previous.y) | |
let p2 = CGPoint(x: point.x - dX, y: point.y) | |
path.addCurve(to: point, controlPoint1: p1, controlPoint2: p2) | |
} | |
path.addLine(to: CGPoint(x: last.x, y: self.frame.height)) | |
path.addLine(to: CGPoint(x: first.x, y: self.frame.height)) | |
shape.path = path.cgPath | |
if let style = style { | |
shape.fillColor = style.fillColor.cgColor | |
if style.colors.count != 0 { | |
let gradientLayer = CAGradientLayer() | |
gradientLayer.startPoint = style.startPoint | |
gradientLayer.endPoint = style.endPoint | |
var colors:[CGColor] = [] | |
for color in style.colors { | |
colors.append(color.cgColor) | |
} | |
gradientLayer.colors = colors | |
gradientLayer.opacity = style.opacity | |
gradientLayer.frame = self.frame | |
shape.mask = gradientLayer | |
} | |
} | |
return shape | |
} | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
guard let dataSource = self.dataSource else { | |
return | |
} | |
if let layer = self.graphLayer { | |
layer.removeFromSuperlayer() | |
} | |
if let shape = self.axisShape { | |
shape.removeFromSuperlayer() | |
} | |
let layer = CALayer() | |
layer.frame = self.layer.frame | |
for graphIndex in 0..<dataSource.numberOfGraphs(in: self) { | |
var values:[Float] = [] | |
for item in 0..<dataSource.graphView(self, numberOfValuesForGraph: graphIndex) { | |
values.append(dataSource.graphView(self, valueForGraphAt: IndexPath(item: item, section: graphIndex))) | |
} | |
let fillStyle:GraphFillStyle? = self.delegate == nil ? nil : self.delegate?.graphView(self, fillStyleForGraph: graphIndex) | |
if let graph = self.fill(from: values, style: fillStyle) { | |
layer.addSublayer(graph) | |
} | |
var color:UIColor = UIColor.white | |
var lineWidth:CGFloat = 1.0 | |
if let delegate = self.delegate { | |
color = delegate.graphView(self, colorForGraph: graphIndex) | |
lineWidth = delegate.graphView(self, lineWidthForGraph: graphIndex) | |
} | |
let strokeStyle:GraphStrokeStyle? = self.delegate == nil ? nil : self.delegate?.graphView(self, strokeStyleForGraph: graphIndex) | |
if let line = self.line(from: values, color:color, lineWidth: lineWidth, style:strokeStyle) { | |
layer.addSublayer(line) | |
} | |
} | |
self.graphLayer = layer | |
self.layer.addSublayer(layer) | |
self.axisShape = self.axis(withX: self.horizontalAxisSegments, Y: self.verticalAxisSegments) | |
if let axisShape = self.axisShape { | |
self.layer.addSublayer(axisShape) | |
} | |
} | |
} | |
class MyViewController : UIViewController, GraphViewDataSource, GraphViewDelegate { | |
var values:[[Float]] = [] | |
override func loadView() { | |
let view = UIView() | |
view.backgroundColor = .white | |
self.view = view | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
let margins = self.view.layoutMarginsGuide | |
for _ in 0..<1 { | |
var values:[Float] = [] | |
for _ in 0..<10 { | |
values.append( Float(arc4random()) / Float(UINT32_MAX) ) | |
} | |
self.values.append(values) | |
} | |
let graph = GraphView(frame: CGRect()) | |
graph.verticalAxisSegments = 9 | |
// graph.horizontalAxisSegments = 9 | |
graph.backgroundColor = UIColor(red: 77/255, green: 84/255, blue: 98/255, alpha: 1.0) | |
graph.dataSource = self | |
graph.delegate = self | |
graph.translatesAutoresizingMaskIntoConstraints = false | |
self.view.addSubview(graph) | |
graph.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true | |
graph.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true | |
graph.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true | |
graph.heightAnchor.constraint(equalTo: graph.widthAnchor, multiplier: 1.0).isActive = true | |
} | |
func numberOfGraphs(in graphView: GraphView) -> Int { | |
return self.values.count | |
} | |
func graphView(_ graphView: GraphView, numberOfValuesForGraph graph: Int) -> Int { | |
return self.values[graph].count | |
} | |
func graphView(_ graphView: GraphView, valueForGraphAt indexPath: IndexPath) -> Float { | |
return self.values[indexPath.section][indexPath.item] | |
} | |
func graphView(_ graphView: GraphView, colorForGraph graph: Int) -> UIColor { | |
return graph == 0 ? UIColor(red: 255/255, green: 48/255, blue: 203/255, alpha: 1.0) : .green | |
} | |
func graphView(_ graphView:GraphView, lineWidthForGraph graph:Int) -> CGFloat { | |
return 2.0 | |
} | |
func graphView(_ graphView: GraphView, fillStyleForGraph graph: Int) -> GraphFillStyle? { | |
let style = GraphFillStyle() | |
style.colors = [UIColor(white: 0, alpha: 1.0), UIColor(white: 0, alpha: 0.0)] | |
style.opacity = 0.5 | |
style.fillColor = .black | |
return style | |
} | |
func graphView(_ graphView: GraphView, strokeStyleForGraph graph: Int) -> GraphStrokeStyle? { | |
return GraphStrokeStyle() | |
} | |
} | |
// Present the view controller in the Live View window | |
PlaygroundPage.current.liveView = MyViewController() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment