Created
June 28, 2020 21:16
-
-
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
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
| // | |
| // 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