Skip to content

Instantly share code, notes, and snippets.

@tobins
Created August 13, 2018 22:27
Show Gist options
  • Save tobins/4c30016b71385f6094698e960fea50e7 to your computer and use it in GitHub Desktop.
Save tobins/4c30016b71385f6094698e960fea50e7 to your computer and use it in GitHub Desktop.
CAShapeLayer as a Graphy (playgrounds)
//: 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