Preparing sample stock data and completing our chart

Defining the data model 🛠

Before defining the data model for our stock data entries, we create a new group called “Model” in our Project navigator. Then, we create a new Swift file called “DataEntry.swift”. Make sure you place this file into the “Model” group.

Inside this file, create a new struct named “DataEntry” which adopts the Identifiable and Codable protocol (We’ll need those protocols for parsing the related JSON later on). We’ll use instances of this struct to represent each data point of our stock. Then, we’ll use the combined data points to create a graph out of them.

Each data point of our stock chart graph should contain its related date and its closing price.

struct DataEntry: Identifiable, Codable {
    let id = UUID()
    let date: Date
    let close: Double
}

Great! This simple data model is everything we need for representing each stock’s graph and information.

Generating sample chart data

Before fetching real-life stock data, we’ll work with some sample data entries to design our chart’s graph. So, let’s create an array containing some random data entries. You can place this array below the DataEntry struct.

let sampleData = [
    DataEntry(date: Calendar.current.date(byAdding: .day, value: -4, to: Date())!, close: 2.33),
    DataEntry(date: Calendar.current.date(byAdding: .day, value: -3, to: Date())!, close: 17.319),
    DataEntry(date: Calendar.current.date(byAdding: .day, value: -2, to: Date())!, close: 13.94),
    DataEntry(date: Calendar.current.date(byAdding: .day, value: -1, to: Date())!, close: 20.4882)
]

The corresponding graph of this sampleData array will look something like this:

Adjusting the UI

Since we have some sample stock data we can work with we can adjust our UI to use the sampleData array instead of fixed values.

Let’s start with our StockListRow view. Instead of using the fixed pricing value, we declare a stockData property and refer to the latest DataEntry instance inside of it:

struct StockListRow: View {
    
    let stockData: [DataEntry]
    
    var body: some View {
        HStack {
            NavigationLink(destination: ContentView()) {
                VStack(alignment: .leading) {
                    //...
                }
                Spacer()
                VStack(alignment: .trailing) {
                    //...
                    Text("$" + String(format: "%.2f", stockData.last?.close ?? 0))
                        .font(.custom("Avenir", size: 26))
                }
            }
        }
    }
}

Then, we provide our initialized StockListRow in our StockList with the sampleData we just generated.

struct StockList: View {
    var body: some View {
        NavigationView {
            List {
                StockListRow(stockData: sampleData)
            }
                .navigationTitle("StockX")
        }
    }
}

Each StockListRow should also display the percentage change we calculate using the last and the first entry inside the provided stock data.

For this purpose, we create a new Swift file named “ViewHelper.swift” and place it into a new Group which we call “Helper”. Inside this file, create the following function:

func getPercentageChange(stockData: [DataEntry]) -> Double {
    if let lastEntryClose = stockData.last?.close, let firstEntryClose = stockData.first?.close {
        return ((lastEntryClose-firstEntryClose)/lastEntryClose)*100
    } else {
        return 0
    }
}

In our StockListRow view, we can use this function to retrieve the percentage change in our stockData.

HStack {
    NavigationLink(destination: ContentView()) {
        //...
        Spacer()
        VStack(alignment: .trailing) {
            Text(String(format: "%.2f", getPercentageChange(stockData: stockData)) + "%")
                .font(.custom("Avenir", size: 14))
                .fontWeight(.medium)
               .foregroundColor(getPercentageChange(stockData: stockData) < 0 ? .red : .green)
            //...
        }
    }
}

The StockList preview should now display the information contained in our sampleData array.

Next, we adjust the layout of our ContentView. Inside this view, as well as in the Header, we also declare a stockData property. 

struct ContentView: View {
    
    let stockData: [DataEntry]
    
    //...
}

struct Header: View {
    
    let stockData: [DataEntry]
    
    //...

}

In our StockListRow, we use the provided stockData and pass it down to the ContentView.

struct StockListRow: View {
    
    let stockData: [DataEntry]
    
    var body: some View {
        HStack {
            NavigationLink(destination: ContentView(stockData: stockData)) {
                //...
            }
        }
    }
}

And in our ContentView, we use the same stockData to pass it down to the Header view.

Header(stockData: stockData)

Don’t forget to provide the ContentView_Previews struct with the sampleData as well.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(stockData: sampleData)
    }
}

Finally, we can use the stockData we passed down from the StockList to the Header the same way we just did inside the StockListRow view itself.

struct Header: View {
    
    let stockData: [DataEntry]
    
    var body: some View {
        HStack(alignment: .bottom) {
            Text("$" + String(format: "%.2f", stockData.last?.close ?? 0))
                //...
            Text(String(format: "%.2f", getPercentageChange(stockData: stockData)) + "%")
                //...
               .foregroundColor(getPercentageChange(stockData: stockData) < 0 ? .red : .green)
        }
            //...
    }
}

Our ContentView preview should now display the same information as our StockList preview!

Drawing the stock graph 📈

Using the sampleData and our drawing knowledge, we have everything we need to start drawing the stock graph. To do this, open the Chart.swift file and create a new struct called “Graph” that adopts the Path protocol and initializes a Path instance.

struct Graph: Shape {
    func path(in rect: CGRect) -> Path {
        
        var path = Path()
        
        
        return path
    }
}

To see what we’re drawing, wrap the Grid inside the Chart view into a ZStack and stack a Graph instance together with a .stroke on top of it.

struct Chart: View {
    var body: some View {
        ZStack {
            Grid()
                .stroke(lineWidth: 0.2)
            Graph()
                .stroke(lineWidth: 2.0)
        }
    }
}

Our Graph shape needs to access the chart data we provide to it, so let’s declare a corresponding property.

struct Graph: Shape {
    
    let dataSet: [DataEntry]

    func path(in rect: CGRect) -> Path {
       //...
    }
}

Accordingly, we need to create such a property for the Chart view that contains the Graph. To do this, write:

struct Chart: View {
    
    let dataSet: [DataEntry]
    
    var body: some View {
        ZStack {
            Grid()
                .stroke(lineWidth: 0.2)
            Graph(dataSet: dataSet)
                .stroke(lineWidth: 2.0)
        }
    }
}

In our ContentView, we can now use the stockData from the StockList to pass it down to the Graph inside the Chart view.

struct ContentView: View {
    
    let stockData: [DataEntry]
    
    var body: some View {
        VStack {
            //...
            Chart(dataSet: stockData)
                .frame(height: 300)
            //...
        }
            .navigationTitle("StockX")
    }
}

Again, don’t forget to provide the preview of our Chart with the sampleData. While doing this, we can also add a .frame to our Chart preview to see how the chart will look in our ContentView.

struct Chart_Previews: PreviewProvider {
    static var previews: some View {
        Chart(dataSet: sampleData)
            .frame(height: 300)
    }
}

Now, we are ready to start drawing the graph.

The “height” (horizontal position) of each data point should be relative to the maximum and minimum close of all data points of the provided dataSet. To retrieve these, we write:

func path(in rect: CGRect) -> Path {
        
    var path = Path()
        
    let max = dataSet.map { $0.close }.max()
    let min = dataSet.map { $0.close }.min()
        
    return path
}

The graph’s starting point should be on the left edge of the invisible rect and with a relative “height” (y-coordinate) depending on the max and min value. To calculate this point, we use the following formula:

func path(in rect: CGRect) -> Path {
        
    //...
        
    let startingPoint = CGPoint(x: 0, y: (1-(CGFloat(dataSet[0].close-(min ?? 0)))/(CGFloat((max ?? 0) - (min ?? 0))))*rect.size.height)

        
    return path
}

Let’s move our “cursor” to this point.

func path(in rect: CGRect) -> Path {
        
    //...
        
    path.move(to: startingPoint)
        
    return path
}

Okay, but how do we draw each line depending on each next value inside the provided dataSet array?

We can do this by cycling through every DataEntry inside the dataSet. While doing this, we can keep track of the index of each element inside the dataEntry by using the enumerated method like this: 

func path(in rect: CGRect) -> Path {
        
    //...
        
    for (index, entry) in dataSet.enumerated() {
                    
    }
        
    return path
}

Next, we need to calculate the x- and y-coordinate for every DataEntry we cycle through. We do this by using the following formulas:

for (index, entry) in dataSet.enumerated() {
    let xValue = rect.size.width*CGFloat(Double(index)/Double(dataSet.count-1))
    let yValue = (1-(CGFloat(entry.close-(min ?? 0)))/(CGFloat((max ?? 0) - (min ?? 0))))*rect.size.height
    
}

Finally, we can add a new line for each entry by using the generated coordinates.

for (index, entry) in dataSet.enumerated() {
    //...
    
    path.addLine(to: CGPoint(x: xValue, y: yValue))
}

Let’s take a look at the Chart preview. 

Awesome! We just drew a perfect graph by using the provided sampleData.

Adding a price legend 🏷

What’s left is adding a legend to our Chart that indicates the pricing level for each horizontal grid line. To do this, add a new struct called “PriceLegend” to the Chart.swift file. This struct also needs to access the dataSet and retrieve the maximum and minimum close value.

struct PriceLegend: View {
    
    let dataSet: [DataEntry]
    let min: Double
    let max: Double
    
    init(dataSet: [DataEntry]) {
        self.dataSet = dataSet
        min = dataSet.map { $0.close }.min() ?? 0
        max = dataSet.map { $0.close }.max() ?? 0
    }
    
    var body: some View {
        
    }
}

Let’s add an instance of this view to our Chart. Since we want it to appear on the Chart’s right sight, we add the .trailing alignment mode to its ZStack.

ZStack(alignment: .trailing) {
    Grid()
        .stroke(lineWidth: 0.2)
    Graph(dataSet: dataSet)
        .stroke(lineWidth: 2.0)
    PriceLegend(dataSet: dataSet)
}

In the PriceLegend view, compose the pricing indicators using a VStack containing several Text and Spacer views. 

var body: some View {
    VStack {
        Spacer()
        Text(String(format: "%.2f", (max-min)*0.75+min))
            .font(.custom("Avenir", size: 14))
            .foregroundColor(.gray)
        Spacer()
        Text(String(format: "%.2f", (max-min)*0.5+min))
            .font(.custom("Avenir", size: 14))
            .foregroundColor(.gray)
        Spacer()
        Text(String(format: "%.2f", (max-min)*0.25+min))
            .font(.custom("Avenir", size: 14))
            .foregroundColor(.gray)
        Spacer()
        Text(String(format: "%.2f", min))
            .font(.custom("Avenir", size: 14))
            .foregroundColor(.gray)
    }
}

Let’s take a look at the Chart preview. Wasn’t that easy?

Adding a gradient layer 

Our chart looks a bit boring, though. To change this, let’s put a gradient layer on top of it, indicating whether the stock goes up (bullish) or down (bearish). 

For drawing such a gradient layer, copy the Graph struct and rename the copy to GraphGradient. Next, insert a GraphGradient shape instance into the Chart’s ZStack.

ZStack(alignment: .trailing) {
    //...
    GraphGradient(dataSet: dataSet)
    PriceLegend(dataSet: dataSet)
}

Currently, or GraphGradient simply mimics the Graph path. But after drawing all the path lines, we want to add another line pointing at the lower-right corner of the invisible rect. Then, we add another line pointing to the lower-left corner before closing the subpath. Therefore, add the following code to your GraphGradient struct:

func path(in rect: CGRect) -> Path {
    
    //...
    path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height))
    path.addLine(to: CGPoint(x: 0, y: rect.size.height))
    path.closeSubpath()
    
    return path
}

By doing that, we draw a new layer on top of our chart, which is currently filled black.

Instead, we want it – depending on whether the stock goes up or down – to be filled with a green or red gradient. 

For checking whether the stock is bullish or bearish, we declare a new function called “bullishBearishGradient” in our ViewHelper.swift file that accepts two Double parameters and returns a SwiftUI Gradient. Make sure the SwiftUI library is imported into the ViewHelper.swift file.

import SwiftUI

//...

func bullishBearishGradient(lastClose: Double, firstClose: Double) -> Gradient {

}

If the provided last closing price is lower than the first closing price, we want to return a red Gradient to represent a bearish stock graph. Otherwise, we return a green Gradient representing a bullish stock graph.

func bullishBearishGradient(lastClose: Double, firstClose: Double) -> Gradient {
    if lastClose < firstClose {
        return Gradient(colors: [Color.red, Color.clear])
    } else {
        return Gradient(colors: [Color.green, Color.clear])
    }
}

Back in our Chart, we can now fill the GraphGradient with the appropriate gradient by using the bullishBearishGradient function.

GraphGradient(dataSet: dataSet)
    .fill(LinearGradient(gradient: bullishBearishGradient(lastClose: dataSet.last?.close ?? 0, firstClose: dataSet.first?.close ?? 0), startPoint: .top, endPoint: .bottom))

Let’s run our app to see if this works!

Recap

Great! After we defined the data model, we generated some sample stock data. By using this data, we accomplished drawing a corresponding chart graph. After that, with just a few lines of code, we accomplished providing our chart with a price legend. Additionally, we cloned and adjusted the Graph shape to create a gradient layer we stacked on top of the Graph.

Next, we are going to fetch real-life stock data by using the free Alphavantage API.

Leave a Reply

Your email address will not be published. Required fields are marked *