Skip to content

Instantly share code, notes, and snippets.

@CodeSlicing
Created June 28, 2020 21:16
Show Gist options
  • Select an option

  • Save CodeSlicing/812c4cf293fb3d6748a78be991c3edf4 to your computer and use it in GitHub Desktop.

Select an option

Save CodeSlicing/812c4cf293fb3d6748a78be991c3edf4 to your computer and use it in GitHub Desktop.
Animated bar chart showing how layout guides can be used to facilitate animation of rectangles
//
// BarChartAnimated.swift
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Created by Adam Fordyce on 28/06/2020.
// Copyright © 2020 Adam Fordyce. All rights reserved.
//
import SwiftUI
import PureSwiftUI
private let gradient = LinearGradient([Color(white: 0.9), Color(white: 0.6)], to: .trailing)
struct BarChartAnimated: View {
@State private var displayingChart = false
var body: some View {
let bars = [1, 2, 3, 4, 3, 9, 2]
return VStack {
ZStack {
AxisShape(displayingChart: displayingChart)
.stroke(Color.gray, lineWidth: 2)
GridLinesShape(numLines: 10, displayingChart : displayingChart, overlap: 0.3)
.stroke(Color.black.opacity(0.5), lineWidth: 0.5)
BarsShape(bars: bars, max: 10, displayingChart : displayingChart, overlap: 0.5)
.fill(gradient)
.shadow(5)
BarsShape(bars: bars, max: 10, displayingChart: displayingChart, overlap: 0.5)
.stroke(Color.black, lineWidth: displayingChart ? 1 : 0)
}
.backgroundIf(displayingChart, Color.blue.opacity(0.3))
.frame(200, 400)
}
.onAppear {
withAnimation(Animation.easeInOut(duration: 1).delay(0.2).repeatForever()) {
self.displayingChart = true
}
}
.edgesIgnoringSafeArea(.all)
}
}
private protocol AnimatableShapeWithDouble: Shape {
var factor: Double {get set}
}
private extension AnimatableShapeWithDouble {
var animatableData: Double {
get {
factor
}
set {
factor = newValue
}
}
}
private struct BarsShape: AnimatableShapeWithDouble {
let bars: [Int]
let max: Int
let factorCalculator: FactorCalculator
var factor: Double
init(bars: [Int], max: Int, displayingChart: Bool, overlap: Double = 0) {
self.bars = bars
self.max = max
self.factor = displayingChart ? 1 : 0
self.factorCalculator = FactorCalculator(bars.count, overlap: overlap)
}
func path(in rect: CGRect) -> Path {
var path = Path()
let halfWidth = rect.widthScaled(1 / (4 * bars.count.asDouble))
var g = LayoutGuide.grid(rect, columns: bars.count + 1, rows: max)
.scaled(.size(1, -1)) // scaled so increasing y goes from bottom to top
for column in 0..<bars.count {
let barFactor = factorCalculator.factorFor(column, for: factor)
let value = bars[column]
let barGridIndex = column + 1
let rectBottomLeading = g[barGridIndex, 0].xOffset(-halfWidth)
// g is now an inverted layout guide (in y since we scaled the y by -1) so
// increasing values of y will go up - "value" in this case
let rectTopTrailing = g[barGridIndex, 0].to(g[barGridIndex, value], barFactor)
.xOffset(halfWidth)
path.rect(from: rectBottomLeading, to: rectTopTrailing)
}
return path
}
}
private struct GridLinesShape: AnimatableShapeWithDouble {
let numLines: Int
let factorCalculator: FactorCalculator
var factor: Double
init(numLines: Int, displayingChart: Bool, overlap: Double = 0) {
self.numLines = numLines
self.factor = displayingChart ? 1 : 0
self.factorCalculator = FactorCalculator(numLines + 1, overlap: overlap)
}
func path(in rect: CGRect) -> Path {
var path = Path()
var g = LayoutGuide.grid(rect, columns: 1, rows: numLines)
.scaled(.size(1, -1)) // invert in y so increasing row index goes up
for row in 1..<g.yCount {
let from = g[0, row]
let to = from.to(g[1, row], factorCalculator.factorFor(row, for: factor))
path.line(from: from, to: to)
}
return path
}
}
private struct AxisShape: AnimatableShapeWithDouble {
var factor: Double
init(displayingChart: Bool) {
self.factor = displayingChart ? 1 : 0
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.line(from: rect.bottomLeading, to: rect.bottomLeading.to(rect.topLeading, factor))
path.line(from: rect.bottomLeading, to: rect.bottomLeading.to(rect.bottomTrailing, factor))
return path
}
}
/**
Calculates the factor between 0 and 1 for a given bucket. If bucket range is 0.3->0.4 and value is 0.35, result will be 0.5
Anything outside the range will be clamped at 0 or 1
*/
private struct FactorCalculator {
let factorBuckets: [(min: Double, max: Double, delta: Double)]
let startStep: Double
let factorPerBucket: Double
let actualRange: Double
init(_ numBuckets: Int, overlap: Double = 0.5) {
let nominalRange = 1 / numBuckets.asDouble
let actualRange = (1 - nominalRange) * overlap + nominalRange
let remainingSpace = 1 - actualRange
let step = remainingSpace / (numBuckets - 1)
var factorBuckets = [(Double, Double, Double)]()
for index in 0..<numBuckets {
let min: Double = index * step
let max: Double = min + actualRange
let delta = max - min
factorBuckets.append((min, max, delta))
}
self.factorBuckets = factorBuckets
self.startStep = step
self.factorPerBucket = actualRange
self.actualRange = actualRange
}
func factorFor(_ index: Int, for value: Double) -> Double {
let factorBucket = factorBuckets[index]
let clampedValue = value.clamped(from: factorBucket.min, to: factorBucket.max)
return (clampedValue - factorBucket.min) / factorBucket.delta
}
}
struct GraphBarsAnimated_Previews: PreviewProvider {
struct GraphBarsAnimated_Harness: View {
var body: some View {
BarChartAnimated()
}
}
static var previews: some View {
GraphBarsAnimated_Harness()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment