iOS development SwiftUI experiment - building custom chart part 2

iOS development SwiftUI experiment - building custom chart part 2

Here we are, in the second part of our iOS development SwiftUI experiment! In the first part, you have created a basic black&white chart with some nice curves. You are going to spice your work up and make it pleasing to the eye. It's ANIMATIONS & GRADIENTS time! 🚀

How to add animations & gradients for your iOS app with SwiftUI

To kick-off building new functionalities, you have to do a small refactor to keep the project clean. SwiftUI is heavily forcing view structure to be composed of smaller pieces, so let's follow this guideline and move your grid to a completely separate view.

struct GridView: View {
    let xStepsCount: Int
    let yStepsCount: Int
    
    var body: some View {
        GeometryReader { geometry in
            Path { path in
                let xStepWidth = geometry.size.width / CGFloat(self.xStepsCount)
                let yStepWidth = geometry.size.height / CGFloat(self.yStepsCount)
                
                // Y axis lines
                (1...self.yStepsCount).forEach { index in
                    let y = CGFloat(index) * yStepWidth
                    path.move(to: .init(x: 0, y: y))
                    path.addLine(to: .init(x: geometry.size.width, y: y))
                }
                
                // X axis lines
                (1...self.xStepsCount).forEach { index in
                    let x = CGFloat(index) * xStepWidth
                    path.move(to: .init(x: x, y: 0))
                    path.addLine(to: .init(x: x, y: geometry.size.height))
                }
            }
            .stroke(Color.gray)
        }
    }
}

OK, it's time for real work. You would like to animate the drawing of the chart path. When the view appears it should nicely slide on the screen. You can use the trim(from: to:) method to achieve this. Let's make some adjustments in the chartBody:

private var chartBody: some View {
        GeometryReader { geometry in
            Path { path in
                path.move(to: .init(x: 0, y: geometry.size.height))
                
                var previousPoint = Point(x: 0, y: geometry.size.height)
                
                self.data.forEach { point in
                    let x = (point.x / self.maxXValue) * geometry.size.width
                    let y = geometry.size.height - (point.y / self.maxYValue) * geometry.size.height
                    
                    let deltaX = x - previousPoint.x
                    let curveXOffset = deltaX * self.lineRadius
                    
                    path.addCurve(to: .init(x: x, y: y),
                                  control1: .init(x: previousPoint.x + curveXOffset,
                                                  y: previousPoint.y),
                                  control2: .init(x: x - curveXOffset,
                                                  y: y ))
                    
                    previousPoint = .init(x: x, y: y)
                }
            }
            .trim(from: 0, to: self.isPresented ? 1 : 0)
            .stroke(
                Color.black,
                style: StrokeStyle(lineWidth: 3)
            )
            .animation(.easeInOut(duration: 0.8))
        }
        .onAppear {
            self.isPresented = true
        }
    }

There is not much to explain. Just trim a path from 0 to 1 in specified animation duration (in this case 0.8s).

To make it work you need @State property which will maintain the animation

@State private var isPresented: Bool = false
iOS development SwiftUI chart animation
To make it work you need @State property which will maintain the animation

Wow! That was super easy and the outcome is awesome! 🔥

But you can't leave the chart black&white, how about adding some gradients? Let's start with the chart line. To fill it in with gradient just change stroke's Color.black

.stroke(
  LinearGradient(gradient: Gradient(colors: [.primaryGradient, .secondaryGradient]),
                 startPoint: .leading,
                 endPoint: .trailing),
  style: StrokeStyle(lineWidth: 3)
)

.primaryGradient, .secondaryGradient are my custom colors added to the Color extension, they can be whatever you want, just use your imagination. 🦄

You can do even better by adding the background to the chart. To do so you can draw a closed path below the actual chart line and fill it with gradients. But first, you have to abstract away a path drawing.

struct LineChartProvider {
    let data: [Point]
    var lineRadius: CGFloat = 0.5
    
    private var maxYValue: CGFloat {
        data.max { $0.y < $1.y }?.y ?? 0
    }
    
    private var maxXValue: CGFloat {
        data.max { $0.x < $1.x }?.x ?? 0
    }
    
    func path(for geometry: GeometryProxy) -> Path {
        Path { path in
            path.move(to: .init(x: 0, y: geometry.size.height))
            
            drawData(data, path: &path, size: geometry.size)
        }
    }
    
    func closedPath(for geometry: GeometryProxy) -> Path {
        Path { path in
            path.move(to: .init(x: 0, y: geometry.size.height))

            drawData(data, path: &path, size: geometry.size)
            
            path.addLine(to: .init(x: geometry.size.width, y: geometry.size.height))
            path.closeSubpath()
        }
    }
    
    private func drawData(_ data: [Point], path: inout Path, size: CGSize) {
        var previousPoint = Point(x: 0, y: size.height)
        
        self.data.forEach { point in
            let x = (point.x / self.maxXValue) * size.width
            let y = size.height - (point.y / self.maxYValue) * size.height
            
            let deltaX = x - previousPoint.x
            let curveXOffset = deltaX * self.lineRadius
            
            path.addCurve(to: .init(x: x, y: y),
                          control1: .init(x: previousPoint.x + curveXOffset, y: previousPoint.y),
                          control2: .init(x: x - curveXOffset, y: y ))
            
            previousPoint = .init(x: x, y: y)
        }
    }
}

So, the drawing logic is now the part of LineChartProvider. It has two accessible methods: path and closedPath. The only difference between them is one additional step for closedPath which makes it closed shape (this lets us fill it properly with gradient). The private drawData method is an actual drawing part of the provider, it's abstracted away to easy reuse between the path and closedPath methods.

Also, you have moved some of the supporting computed properties from ChartView to LineChartProvider.

With those improvements ready, you can use it in the chart:

private var chartBody: some View {
        let pathProvider = LineChartProvider(data: data, lineRadius: lineRadius)
        return GeometryReader { geometry in
            ZStack {
                pathProvider.closedPath(for: geometry)
                    .fill(
                        LinearGradient(gradient: Gradient(colors: [.white, Color.primaryGradient.opacity(0.6)]),
                                       startPoint: .bottom,
                                       endPoint: .top)
                    )
      
                pathProvider.path(for: geometry)
                    .trim(from: 0, to: self.isPresented ? 1 : 0)
                    .stroke(
                        LinearGradient(gradient: Gradient(colors: [.primaryGradient, .secondaryGradient]),
                                       startPoint: .leading,
                                       endPoint: .trailing),
                        style: StrokeStyle(lineWidth: 3)
                    )
                    .animation(.easeInOut(duration: 0.8))
            }
            .onAppear {
                self.isPresented = true
            }
        }
}
iOS development SwiftUI colorful chart
iOS development SwiftUI colorful chart

Neat! But you can do even better. So far the animation doesn't look natural. You have to add animation to the background as well!

ZStack {
    // Background
    pathProvider.closedPath(for: geometry)
      .fill(
        LinearGradient(gradient: Gradient(colors: [.white, Color.primaryGradient.opacity(0.6)]),
                       startPoint: .bottom,
                       endPoint: .top)
      )
      .opacity(self.isPresented ? 1 : 0)
      .animation(Animation.easeInOut(duration: 1).delay(0.6))
                
      // Chart
      pathProvider.path(for: geometry)
        .trim(from: 0, to: self.isPresented ? 1 : 0)
        .stroke(
          LinearGradient(gradient: Gradient(colors: [.primaryGradient, .secondaryGradient]),
                         startPoint: .leading,
                         endPoint: .trailing),
          style: StrokeStyle(lineWidth: 3)
        )
        .animation(Animation.easeInOut(duration: 0.8).delay(0.2))
}

This is way better! Manipulate the opacity of the background to fade it in nicely. Moreover, you are using the delay method to chain animations.

iOS development SwiftUI colorful chart animation
iOS development SwiftUI colorful chart animation

At the beginning, it seemed very complicated and complex but it quickly turned out to be easy peasy thanks to the SwiftUI magic. Although I am satisfied with the overall outcome, there are plenty of things that can be improved. It was the last part of the iOS development SwiftUI experiment, but you have tons of other possibilities to discover. Good luck.

Custom software development company that specializes in web and mobile applications development.

Contact

ul. Nadwiślańska 1/10 30-527 Kraków, Poland

[email protected]

+48 795-856-491

See our location on map

Details

VAT-UE: PL6793159393

NIP: 6793159393

REGON: 368770514

KRS: 0000703502

Copyright © 2018-2020 CrustLab Sp. z o.o. All rights reserved.