You really learned a lot about developing apps with SwiftUI. For example, we talked about the basic functionality of SwiftUI, how to compose complex interfaces, how to interact with user input, how to save data, how to communicate with web services, how to draw in SwiftUI, how to works with maps and so much more. We gave you the tools you need. Now, it’s time to create your own, custom apps.
However, More chapters are currently being planned. For example:
How to create Augmented Reality apps with SwiftUI and ARKit
Sign in with Apple
And more!
We will notify you about every update by writing you a mail, so stay tuned and make sure you check out your spam folder as well. If you have any wishes or suggestions on what you would like to learn, make sure you send us a mail or leave a DM on Instagram.
For inspiration, more content about iOS development and tips for iOS developers, follow us on Instagram and Twitter.
Your ContentView preview should now look like this:
Depending on the selected time frame, we want our Chart and Header to use a different array containing only the specific entries. For this purpose, we need to add two more arrays to our DownloadManager.
var weeklyEntries = [DataEntry]()
var dailyEntries = [DataEntry]()
Thus, when fetching the stock data, we need to return weekly and daily entries as well.
Next, we need to adjust how we filter the data in our downloadJSON function. Delete the filteredEntries variable and append the data filtered using the isInLastNDays method to the declared weeklyEntries array.
While cycling through the dataEntries we append all entries from the same day by adding the following conditional block to the for-in loop:
for entry in dataEntries {
if Calendar.current.isDate(entry.date, equalTo: lastDateOfData, toGranularity: .day) {
dailyEntries.append(entry)
}
//...
}
Perfect! Back in our ContentView, we can now either use the weeklyEntries or the dailyEntries array when initializing the Chart and Header depending on the timeFrameChoice.
Let’s run our app to see if that works. Awesome! We’re now able to switch between a daily and weekly time frame by using the related bar.
Finishing our app ✅
Let’s finish our app by adding a dynamic navigation bar title to each ContentView indicating the currently represented stock. To do this, add a new property to your ContentView called “stockSymbol”.
let stockSymbol: String
Next, use the String assigned to the stockSymbol for the .navigationTitle modifier.
VStack {
//...
}
.navigationTitle(stockSymbol)
In our ContentView_Previews struct, we add the corresponding stockSymbol argument and enter a sample String.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(stockSymbol: "AAPL", downloadManager: DownloadManager(stockSymbol: "AAPL"))
}
}
We also need to add the stockSymbol parameter when initializing the ContentView in our StockListRow. For this purpose, declare a corresponding property in the StockListRow struct as well. While doing this, also add a property named “stockName”.
let stockSymbol: String
let stockName: String
Now we can use the stockSymbol when initializing the ContentView inside the StockListRow. Additionally, we use the stockSymbol and stockName instead of current placeholder Text views.
If we run the app now and navigate to the ContentView, you see that the navigation bar displays the provided stock symbol.
That’s it! If you want, you can also add some other StockListRow instances into the StockList by providing each one with the stockName and specific stockSymbol (Just google “Stock Symbol *Stock Name* stock symbol to find the right one.)!
Conclusion 🎊
Great, we finished our StockX app!
You have learned a lot in this chapter. First, we have dealt with how to draw our own shapes in SwiftUI. Then, we used this knowledge to prepare data visually in charts.
Finally, we used the Alpha Vantage API to provide our app with real-life stock data.
As a suggestion, you can try to add more time frame options to the app or display date information on the charts’ x-axis.
For gathering real stock data, we’ll be using the free Alpha Vantage API. For fetching data over this API, you need to register for a free API key, as we did in chapter 13.
Note: The free API plan allows you to make up to 5 API requests per minute and 500 requests per day.
Visit www.alphavantage.co and click on “Get your free API key today”. Then, fill out the contact form and click on “Get Free API Key”.
A text shows up, showing you your personal API key. Let’s save this key by creating a new Swift file in our “Helper” group and calling it “DownloadHelper.swift”.
Inside this file, declare a constant called “apiKey” and assign the key you just generated to it as a String.
let apiKey = "*your API key*"
Similar to the Flickr API, the Alpha Vantage API allows us to request information about specific stocks by sending a request in the form of a URL. We then receive the requested data in JSON format.
Visit the Alpha Vantage API documentation to learn what types of requests are possible. For now, we want to gather financial data about a specific stock on a daily basis. According to the section “Daily” in the documentation, we can use several URL parameters for specifying the request.
A typical URL would look like this:
In our DownloadHelper.swift file, let’s create a function that generates the URL for us. Since the only dynamic value should be the stock symbol, the function is as follows:
Paste the returned String to your preferred browser and replace the stock symbol with, for instance, “AAPL” for Apple, Inc. and your personal API Key. If you open this URL, you’ll see what the JSON structure looks like.
Let’s incorporate the resulting JSON structure into an appropriate data model we’ll use when fetching the JSON later on. We already discussed this topic in chapters 11 and 13, so make sure you check them out if you’re not sure what’s going on. Again, you can use the quicktype.io tool to accomplish this as we did in the last chapter.
Now, let’s create a new Swift file named “JSONModel.swift”, place it into the “Model” group and insert the following struct (note that TimeSeries is a substruct of TimeSeriesJSON).
struct TimeSeriesJSON: Decodable {
let timeSeries: [String: TimeSeries]
private enum CodingKeys : String, CodingKey {
case timeSeries = "Time Series (15min)"
}
struct TimeSeries: Decodable {
let open, close, high, low: String
private enum CodingKeys : String, CodingKey {
case open = "1. open"
case high = "2. high"
case low = "3. low"
case close = "4. close"
}
}
}
Setting up the Download Manager
We need to create a download manager to fetch and parse the JSON data and to notify observing views once we are finished. For this purpose, create a new Swift file named “DownloadManager.swift”, place it into the “Model” group as well and import the SwiftUI framework. In this file, create a class also named “DownloadManager” that conforms to the ObservableObject protocol.
import SwiftUI
class DownloadManager: ObservableObject {
}
In our DownloadManager class, we need to declare a property that holds an array containing all the fetched and parsed financial information as DataEntry instances.
class DownloadManager: ObservableObject {
var dataEntries = [DataEntry]()
}
As said, once we finished fetching the data, we need to notify all observing views. For this purpose, let’s add a corresponding @Published property to our DownloadManager.
@Published var dataFetched = false
In our ContentView, replace the stockData property with an instance of the DownloadManager as an @ObservedObject. Now we can use the dataEntries in the downloadManager for passing it down to the Header and Chart:
struct ContentView: View {
@ObservedObject var downloadManager: DownloadManager
var body: some View {
VStack {
Header(stockData: downloadManager.dataEntries)
Chart(dataSet: downloadManager.dataEntries)
.frame(height: 300)
//...
}
.navigationTitle("StockX")
}
}
Make sure you adjust the ContentView_Previews struct as well.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(downloadManager: DownloadManager())
}
}
In our StockListRow struct, we also need a DownloadManager to pass it down to the ContentView and display the pricing information.
In our StockList, we create the actual DownloadManager instance we pass all the way down to the ContentView when initializing the StockListRow.
List {
StockListRow(downloadManager: DownloadManager())
}
Okay, let’s run our app to see if that works. Oops! After we tap on the StockListRow to navigate to the ContentView our app crashes because of an “Index out of range” error in our Chart view! This is because that Graph, GraphGradient, and PriceLegend try to utilize elements in a dataSet that is currently empty. Therefore, we want to initialize these shapes only under the condition that the dataSet isn’t empty:
struct Chart: View {
let dataSet: [DataEntry]
var body: some View {
ZStack(alignment: .trailing) {
if !dataSet.isEmpty {
//...
}
}
}
}
If you re-run the app, you see that everything works fine now, and only the Grid gets displayed when navigating to the ContentView.
Great! Now we’re finally ready to fetch real stock data by using the Alpha Vantage API.
Fetching real-world stock data ⬇️
In our DownloadManager class, let’s create a function to fetch the stock data using the Alpha Vantage API.
private func fetchData(stockSymbol: String) {
}
Before starting the download, we need to make sure that our dataFetched property is false.
As we did in the last chapter, we mark this function with the asyncthrows keywords to tell SwiftUI that we want this function to be performed asynchronous and that it can throw errors.
Accordingly, we need to define an enum conforming to the Error protocol and insert it into our DownloadManager. Two errors are conceivable here: First, that we cannot generate a valid URL or second, that we cannot generate usable DataEntry instances.
enum FetchError: Error {
case badURL
case badEntries
}
Let’s call the downloadJSON function and assign its return to the dataEntries, weeklyEntries and dailyEntries properties when calling the fetchData function by writing:
As we have learned in the last chapter, asynchronous functions cannot be directly embedded in a synchronous context like our fetchData function. So to make this possible we have to use a Task again.
After we successfully parsed the JSON, we want to cycle through every timeSeries instance in it to create a DataEntry out of it and append it to the dataEntries array.
But before we can do this, we need to make sure that we’re able to create Date objects out of the Strings contained in the parsedJSON. To do this, we need a Date extension that lets us format such a String into a Date.
For this purpose, create a new Swift file called “Extensions.swift” and place it into the “Helper” group. Inside this file, add the following Date extension.
extension Date {
init(_ dateString:String, dateFormat: String) {
let dateStringFormatter = DateFormatter()
dateStringFormatter.dateFormat = dateFormat
dateStringFormatter.locale = Locale.init(identifier: "en_US_POSIX")
let date = dateStringFormatter.date(from: dateString)!
self.init(timeInterval:0, since:date)
}
}
Back in our downloadJSON function, we can now create a DataEntry out of every timeSeries element inside the parsedJSON.
private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {
//...
for timeSeries in parsedJSON.timeSeries {
dataEntries.append(DataEntry(date: Date(timeSeries.key, dateFormat: "yyyy-MM-dd HH:mm:ss"), close: (timeSeries.value.close as NSString).doubleValue))
}
}
We can check if we cycled through every instance by comparing the number of our DataEntries to the number of timeSeries in the parsedJSON. If these are equal, we know that we’re finished with the downloading and parsing process.
private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {
//...
for timeSeries in parsedJSON.timeSeries {
dataEntries.append(DataEntry(date: Date(timeSeries.key, dateFormat: "yyyy-MM-dd HH:mm:ss"), close: (timeSeries.value.close as NSString).doubleValue))
}
if dataEntries.count == parsedJSON.timeSeries.count {
}
}
We need to make sure that the elements in our DataEntries array follow the correct order depending on their Date properties.
private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {
//...
for timeSeries in parsedJSON.timeSeries {
dataEntries.append(DataEntry(date: Date(timeSeries.key, dateFormat: "yyyy-MM-dd HH:mm:ss"), close: (timeSeries.value.close as NSString).doubleValue))
}
if dataEntries.count == parsedJSON.timeSeries.count {
dataEntries.sort(by: {$0.date.compare($1.date) == .orderedAscending})
}
}
Filtering the data entries
Right now, our dataEntries array contains every time series we extracted from the parsed JSON. However, we only want to display the stock information for the last seven days. Therefore, we need to filter the dataEntries before turning the dataFetched property to true.
For this purpose, we need to add some more lines of codes to our downloadJSON function before returning the dataEntries.
But how do we know which time series in our DataEntries array are from within the last seven days? It’s simple, we just need to refer to the last element in our dataEntries array and compare each remaining element to it.
We need to make sure that there is actually a last element we can refer to. To do this, we use a guard statement in our filterDataEntries function. After that, we create a new array that will only contain the filtered entries.
To compare each entry to the lastDateOfData we need to create another Date extension. So, let’s open the Extensions.swift file and add another function to it.
extension Date {
//...
func isInLastNDays(lastDate: Date, dayRange n: Int) -> Bool {
let startDate = Calendar.current.date(byAdding: .day, value: -n, to: Date())!
return (min(startDate, lastDate) ... max(startDate, lastDate)).contains(self)
}
}
Using this function, we can check if a specific Date is within the last n-days of another given Date.
Back in our downloadJSON function: We create a new filteredEntries array, cycle through every element in the dataEntries array, compare its date to the lastDateOfDate and only add those entries to the filteredEntries that are in the specified time range of 7 days. Then we assign the content of the filteredEntries to the dataEntries.
private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {
//...
guard let lastDateOfData = dataEntries.last?.date else { throw FetchError.badEntries }
var filteredEntries = [DataEntry]()
for entry in dataEntries {
if entry.date.isInLastNDays(lastDate: lastDateOfData, dayRange: 7) {
filteredEntries.append(entry)
}
}
dataEntries = filteredEntries
}
Finally, we can return the – now filtered – dataEntries.
Now back to the fetchData function that calls the downloadJSON function: after we waited for the downloadJSON to finish, we notify all observing views by turning the dataFetched property to true. We do this asynchronously as well. For this purpose, we use the DispatchQueue.main.async wrapper.
We want to start fetching the stock data once the DownloadManager gets initialized. To do this, add an init function to the DownloadManager class that calls the fetchData function by using a provided stock symbol.
Awesome! We’re finished preparing our DownloadManager for downloading, parsing, and filtering the JSON that contains the relevant financial information.
Next, we need to adjust our interface to reflect the relevant data once it’s available.
Displaying fetched data in our UI 👁
Now, we need to adjust our views to display the fetched data. To do this, provide the StockListRow instance in your StockList with the stockSymbol that the downloadManager should use to call the fetchData function.
List {
StockListRow(downloadManager: DownloadManager(stockSymbol: "AAPL"))
}
Next, we need to provide the ContentView_Previews struct with such a stockSymbol as well.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(downloadManager: DownloadManager(stockSymbol: "AAPL"))
}
}
Finally, we want our ContentView to display the Chart only once the data got fetched. To do this, we utilize the dataFetched property like this:
Okay, the moment has come: Let’s run our app to see if everything works fine.
Awesome! Just a few moments after we launch the app, our DownloadManager fetches the pricing information about the specified stock by accessing the Alpha Vantage API. Once the process is finished, our StockList and ContentView get notified, and they eventually show us the relevant pricing information and the corresponding chart graph!
Recap
Wow, that was a lot of work. But we are almost finished with our StockX app.
So far we’ve learned how to use the Alpha Vantage API to download relevant financial information and parse the corresponding JSON for our purposes.
We now use actual stock information to display the corresponding charts on a weekly basis.
Finally, we will add a way to display the chart on a daily basis as well.
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:
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.
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.
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:
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.
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.
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:
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.
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.
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.
It’s time to learn drawing in SwiftUI! With the following knowledge, you will be able to design your own custom graphics, vectors, and forms. Before we start, let’s create a new SwiftUI view file, name it “Chart.swift” and drop it into the “Views” group.
Let’s take a quick look at SwiftUI’s prebuilt shapes. SwiftUI provides us with some basic shapes we can directly use for our views. These are:
Rectangle
RoundedRectangle
Ellipse
Circle
Capsule
You already got to know most of them, for instance, when we initialized a Rectangle in our ContentView earlier. You can manipulate these shapes, for example, by applying different colors, adding rounded corners, etc.
If you want to learn more about them, take a look at this tutorial on www.BLCKBIRDS.com.
But we can also define and create our own shapes by using so-called paths. In a nutshell, you can imagine a path like a set of drawing instructions, including lines, curves, and other segments like arcs. This is why a shape is doing nothing else than using a specific path to define its appearance.
Let’s try to draw a simple square by using such a path. Start with replacing the default Text view in our Chart with a Path instance followed by a corresponding closure. Inside this closure, we can define the way our Path should go.
By default, SwiftUI will fill out the resulting view with black color. While drawing the Path, however, I prefer to only see the outer border of the resulting view for having a better overview of what I’m drawing. To do this, we can use the .stroke modifier.
struct Chart: View {
var body: some View {
Path { path in
}
.stroke()
}
}
Let’s draw our square by adding several lines to our Path. We can do this by using absolute x- and y-coordinates. Before drawing the first line, we move the “cursor” to our imagined square’s upper-right corner. Then, we add a line that points to the lower-right corner and another line pointing to the lower-left corner.
You see that two lines got added to our SwiftUI preview!
Let’s finish the square by adding a third line pointing to the upper-left corner. We could close the square by adding one last line pointing to where we started, but we can also close the Path “automatically” by using the .closeSubPath modifier. Now that we are done defining our square path, we can fill it out by deleting the .stroke modifier again.
You already know that shapes also consist of paths. Because of this and for reusability purposes, we can simply convert our square Path to such a shape by declaring a struct below our Chart struct, which conforms to the Shape protocol.
struct MySquare: Shape {
}
The only requirement for the Shape protocol is having a path function that draws the actual shape. We can simply use the Path we just created like this:
Now, we can use our MySquare shape inside our SwiftUI view!
struct Chart: View {
var body: some View {
MySquare()
}
}
However, our MySquare shape still uses the absolute coordinates we defined earlier. Instead, we want to make it dynamic to transform it by adding a .frame modifier to the MySquare instance inside our SwiftUI view.
We can use the rect parameter of our MySquare’s path function for this. The rect is like an invisible scratchpad inside which we can draw our square and can be transformed by passing a specific frame to it when initializing the resulting shape.
So, let’s replace the fixed coordinates with the corresponding points of the invisible rect.
Let’s talk through it very quickly. First, we move our cursor to the very upper-right edge of our invisible rectangle.
Then, we tell our path to draw a line to another point using the rect’s width for both, the x- and y-coordinates (remember, we want to ensure it’s a square). Then, we’re going back to the lower-left corner, followed by the upper-left corner before closing the subpath.
Awesome, now our MySquare shape is dynamic, and we can adjust its size by using the .frame modifier!
MySquare()
.frame(width: 250, height: 250)
If you want, you can delete the MySquare struct and its corresponding instance inside the Chart now since we won’t need it anymore.
Drawing the chart grid 📐
Let’s utilize what we just learned to draw the grid for our chart. The grid should consist of three vertical and three horizontal lines.
Let’s start by creating a new struct called “Grid” that adopts the Shape protocol and initializes a Path.
We’ll practice our drawing skills when creating the graph for our chart later on. For now, let’s take a rest and look at what we’ve accomplished so far.
Recap
We’ve prepared the UI for our StockX apps and learned the basics about drawing in SwiftUI. We’ve learned how we can define paths for creating custom shapes. If you want to learn more about drawing in SwiftUI, make sure you check out this tutorial on our website where we discuss this topic in more detail.
Next, we will create the data model for our stock data and generate some random entries we can use to display a sample stock chart graph. By doing this, we will also practice our SwiftUI drawing skills.
Deepen your knowledge about Concurrency in SwiftUI and parsing JSON data
What we’ll create 🚀
In this chapter, we’re going to create a stocks app with real stock data. We are going to fetch that data by using the free Alphavantage API. Furthermore, we will learn how to draw graphics in SwiftUI and use this knowledge to create beautiful line charts. StockX will be able to fetch requested stock data and display it using the time frame specified by the user.
At the end of this chapter, our app will look like this:
Preparing the UI 🎨
As always, we start by creating a new Xcode project and naming it “StockX”. After you created a new project, open the ContentView.swift file, since we’ll use it for displaying the chart’s interface. We start building the StockX app by preparing a simple and static interface to get a feeling for how our app will look like.
Since we have already covered the relevant topics, we will go through this part very quickly.
At the top of our ContentView’sbody, we want to insert a header to display the latest price of a particular stock combined with the percentage price change over a specific time period. For now, we’ll use static data.
Each stock chart should be embedded into a stock list.
For this purpose, create a new SwiftUI view file and name it “StockList.swift”. For keeping an overview of our project, create a new group in the Project Navigator by right-clicking and selecting “New Group”. Call this group “Views” and drag-and-drop both SwiftUI views into it.
Make sure the StockList.swift file is still opened. Then, wrap the default Text view into a List embedded into a NavigationView. Let’s use .plain as the .listStyle
struct StockList: View {
var body: some View {
NavigationView {
List {
Text("Hello, World!")
}
.listStyle(.plain)
.navigationTitle("StockX")
}
}
}
Let’s add the same .navigationTitle to our ContentView.
Next, we define the “blueprint” row for our List. Each row should contain the essential information of the specific stock (its stock symbol, the company’s name, and the latest pricing information) and link to the corresponding ContentView. Therefore, we replace the Text view in our StockList with:
Finally, we also extract this HStack as a subview and name it “StockListRow”.
NavigationView {
List {
StockListRow()
}
.listStyle(.plain)
.navigationTitle("StockX")
}
That’s it! This is how your StockList preview should look like now:
Finally, we want our app to open the StockList view instead of our ContentView when launching. To achieve this, we go to our StockXApp struct and replace the ContentView with our StockList:
@main
struct StockXApp: App {
var body: some Scene {
WindowGroup {
StockList()
}
}
}
Great! We’ve prepared the basic layout for our StockX app. Now it’s time to learn how to draw in SwiftUI and use this knowledge to create line charts.
Let’s add a State property to our PhotoGrid containing the image URLs as a String array.
@State var imageURLs = [String]()
Of course, our PhotoGrid also needs to know the coordinates of the location from which the images should be downloaded.
struct PhotoGrid: View {
var latitudeToSearchFor: Double
var longitudeToSearchFor: Double
@State var imageURLs = [String]()
var body: some View {
//...
}
}
Accordingly, we have to update our PhotoGrid_Previews struct …
struct PhotoGrid_Previews: PreviewProvider {
static var previews: some View {
PhotoGrid(latitudeToSearchFor: 48.864716, longitudeToSearchFor: 2.349014)
}
}
Note: Exceptionally we can force-unwrap here because we can be sure that an annotation exists (without it, we couldn’t even open the PhotoGrid!)
Back to our PhotoGrid: We want to start downloading and parsing the JSON data when the PhotoGrid appears. To do this, we apply the .onAppear modifier, call the fetchImageURLs function and assign the returned array to our imageURLs State.
var body: some View {
GeometryReader { geometry in
//...
}
.onAppear {
imageURLs = fetchImageURLs(fromFlickrURL: generateFlickrURL(latitude: latitudeToSearchFor, longitude: longitudeToSearchFor, numberOfPhotos: numberOfPhotos))
}
}
However, Xcode prompts an error message again, this time reading: “‘async’ call in a function that does not support concurrency”. This is because we try to embed an asynchronous function inside a SwiftUI view that works synchronously. However, as we learned in the last section, asynchronous code only works within an asynchronous context. For this purpose, SwiftUI provides a way to embed asynchronous code by using the “Task” wrapper.
Load and display images asynchronously using AsyncImage views 🖼🔁
Next, we want to use the imageURLs array to load the individual images in the PhotoGrid. This process should also be done asynchronously to save system resources and not block the interface until all 400 images have been loaded. Since iOS 15 AsyncImage views are available for this purpose. An AsyncImage only needs the corresponding URL and then handles the download and rendering process for us.
So let’s adjust our ForEach loop to create an AsyncImage for each string in the imageURLs.
Okay, let’s see if this works. Run the app, drop a pin, and tap on it. The PhotoGrid opens and after a few moments, we can watch our AsyncImage views fetching the images from Flickr!
Using the fetchImageURLs function, we receive a JSON, which we parse. By using the extracted values, we generate the URLs for each image to be downloaded. These URLs are then used to download the corresponding images asynchronously using AsyncImage views.
The cool thing about the combination of our LazyVGrid and AsyncImage views is that it doesn’t load all 400 images at once, but only loads as many as are needed to display them seamlessly on the screen. You can notice this by scrolling down fast and watching the newly rendered AsyncImage views loading.
Handling different search phases
The search for images in the Flickr database can go through different phases. Right now we have covered only one of these phases, namely that images have been successfully found in the database and a corresponding JSON is available.
However, there is also the phase when the search is still in progress. In the meantime, the user is still staring at an empty screen. Finally, there may be a situation where no images were found at all.
To represent these three possibilities we add a corresponding enum to our PhotoGrid.
enum SearchStatus {
case noImagesFound
case imagesFound
case searching
}
Finally, we keep track of the phase using an appropriate State property. By default, we assume that we are still in the search and fetch process.
@State var searchStatus: SearchStatus = .searching
Next, we modify our Task in the .onAppear modifier to change the searchStatus accordingly:
Depending on whether we are currently searching for images in the database, have successfully found some in the database, or none are available, we want to adjust the content of the PhotoGrid.
Accordingly, we use a switch statement embedded into a VStack inside the GeometryReader of our PhotoGrid and display the ScrollView with its contents only when the .imagesFound case is given.
While the search process is still running, on the other hand, we want to display a simple spinning activity indicator.
case .searching:
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
If no images were found at the given location, we want to display a corresponding hint to the user.
case .noImagesFound:
Image(systemName: "questionmark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geometry.size.width/12, height: geometry.size.width/8)
Text("No photos found at this location ):")
.padding()
Let’s see if this works. When we run the app and search for images in, say, a densely populated city, we first see an activity indicator. Only after the JSON has been fetched and parsed, the searchStatus changes and we see the ScrollView with the LazyVGrid accordingly.
If, on the other hand, we set a needle in the middle of the Atlantic Ocean, we will probably not find any images and we will get a corresponding indication.
Conclusion 🎊
Great! We just finished creating our City Explorer App. And damn, we learned a lot of stuff!
We dived into working with maps in SwiftUI, learned how to access the user’s location, and how to let the user drop a pin annotation at a specific location. We then used the geographical coordinates of this location to search for photos that were shot by Flickr users in a certain radius around this place.
We used the Flickr API to download these photos and present them inside a Grid view. While the photos are being downloaded, we are presenting a progress bar indicating how many photos have already been fetched!
To download photos shot near the selected location, we use the photo platform Flickr. On this platform, users can upload their pictures along with their EXIF data (e.g., including the GPS coordinates). Flickr allows us to access these images by using the Flickr API.
To learn more about the Flickr API and its functionalities, see the Flickr Developer Guide.
To use the API, we first need to apply for an API key. Don’t worry, for non-commercial projects, this is free. You only need to create a free Flickr account.
To generate your personal API key, open this page and click on “Request an API Key”. Next, you need to log into your Flickr account or create one for free.
After you have logged in, you can create a Non-Commercial Key.
Attention: Non-commercial applies to this project only as long as you use it only for learning and testing purposes. As soon as you publish an app that uses the Flickr API, you may need to apply for a commercial key!
Next, you have to enter the name of your app and briefly describe what it’s about. Also, check the boxes to indicate that you respect third parties rights and agree to the Flickr API terms. Then click on “Submit”.
Next, you’ll see the personal API key that Flickr has generated for you.
Since we will need this key all the time, we will add it to our Xcode project. To do this, we create a new Swift file and name it “APIHelper.swift”. In this file, we declare a constant called “apiKey” and copy-paste the just generated API key as a String.
let apiKey = "*YOUR API KEY*"
How to call the API
But how do we send a request to Flickr to download images? Flickr provides us with many different API methods to do this. For our purpose, we need the “flickr.photos.search” method. To see how it works, open the following link.
Here we can select different parameters we want to use to download suitable images. First, checkmark “apiKey” and use your API key as the value. We want to download pictures from a specific location. Thus, mark “lat” and “lon” as well. For testing purposes, you can use the (decimal) Latitude “48.864716” and the (decimal) Longitude “2.349014”. Also, we only want to download pictures from a certain radius around the given location.
So for example use “1” as “radius” and “km” as “radius_units”. Eventually, we want to download only a limited number of images. To do this, enter “40” as the value of the argument “per_page”. Important: Select “JSON” as the output format and select the option “Do not sign call”.
To perform the test API request, click “Submit”!
As you can see, Flickr returns a bunch of data as JSON.
Note: JSON is a data format that has a tree structure and is easy for humans to read. To use the data from a JSON, we need to extract it properly. This process is called parsing. We can use the parsed data to download the individual images (we will learn how this works in a moment). At the moment, the most interesting part of our test request is the URL generated at the bottom of the page. The URL consists of the different arguments of our request. We can use this URL structure for our app to make dynamic requests for different locations.
Thus, create a function named “generateFlickrURL” below the apiKey constant. We want to specify the longitude, longitude, and the number of images to be downloaded for each request.
Using this function, we can now generate the matching request URL, which returns the corresponding data as JSON.
Fetching the JSON data ⬇️
To receive the JSON with the generateFlickrURL, parse it, and finally download the images, we create a new file called “DownloadHelper.swift”.
First, we set the number of photos to be downloaded by declaring a constant in our DownloadHelper.swift file.
let numberOfPhotos = 400
Next, we create the function that we’ll use to fetch the URLs for every Image that should be downloaded. We mark this function with “throws” to handle potential errors.
To handle those potential errors, we need to create an enum conforming to the Error protocol. Fortunately, conforming to it doesn’t require us to add any specific properties or methods. And since we are only facing the possible error of a bad URL, we only need to define one error case. Thus, let’s add an appropriate enum to our DownloadHelper.swift file.
enum FetchError: Error {
case noURL
}
Let’s continue with trying to generate the URL that returns the JSON data. If this fails we throw our noURL error we just defined.
Xcode prompts an error message reading: “‘async’ call in a function that does not support concurrency”. What does this mean?
For this purpose, we need to understand the difference between synchronous and asynchronous functions first. The system of the device our app runs on uses so-called threads to perform various processes, i.e. to perform a function defined by us. You can imagine threads as working units of the system.
A “normal” function is a synchronous function. A synchronous function blocks the thread until it finishes its work. As long as the thread is blocked, it cannot perform any other task. So a synchronous function is only suitable for fast executable and lightweight tasks.
An asynchronous function, on the other hand, does not block the thread. An asynchronous function can suspend, meaning giving control of the thread to the system that decides what is most important (called awaiting). At some time the system will resume the function (called resuming). This is especially useful for more complex tasks that may take a long time to complete. These include, in particular, networking processes whose speed depends on Internet performance or resource-intensive computations.
One such task is our URLSession. In particular, it depends on the user’s Internet connection how long it takes us to receive a response from the Flickr server. Therefore, it is necessary that we tell Swift to perform the URLSession asynchronously. However, this is only possible in an asynchronous context. This means that our fetchImageURLs must also be marked as asynchronous. And to do this, we mark our fetchImageURLs as asynchronously using the async keyword.
Now we can use the await keyword to tell Swift to perform the URLSession asynchronously as well. At this point, we can also omit the response because we won’t need it for this function.
The system now decides when to release the thread to perform the URLSession. We then just have to wait until we get the JSON back and then assign it to our data constant (usually this should only take a few seconds of course).
Creating the data model for our JSON data 🛠
Before we can parse the JSON data we retrieve, we need to create a corresponding data model. Take a look at the sample JSON data we before when testing the “flickr.photos.search” method.
To access values nested in the JSON we need to create a data model reflecting the structure of the JSON. Fortunately, that’s super simple using the quicktype.io tool. If we copy and paste the sample JSON it automatically generates a fitting Swift data model for us.
Let’s copy & paste this data model into our DownloadHelper.swift file:
struct Welcome: Codable {
let photos: Photos
let stat: String
}
struct Photos: Codable {
let page, pages, perpage, total: Int
let photo: [Photo]
}
struct Photo: Codable {
let id, owner, secret, server: String
let farm: Int
let title: String
let ispublic, isfriend, isfamily: Int
}
Parsing the JSON data ✂️
Let’s continue with parsing the received JSON. We can parse the received JSON data by using a JSONDecoder instance and our created data model.
func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {
guard let url = URL(string: fromFlickrURL) else { throw FetchError.noURL }
let request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: request)
let parsedJSON = try JSONDecoder().decode(Welcome.self, from: data)
}
We can now cycle through every element in the photo set and use the contained values to build each corresponding photo URL String.
func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {
//...
var fetchedImageURLs = [String]()
for photo in parsedJSON.photos.photo {
let photoURL = "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_b.jpg"
fetchedImageURLs.append(photoURL)
}
}
Finally, we return the fetchedImageURLs array:
func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {
//...
var fetchedImageURLs = [String]()
for photo in parsedJSON.photos.photo {
let photoURL = "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_b.jpg"
fetchedImageURLs.append(photoURL)
}
return fetchedImageURLs
}
That’s it! We have everything we need to fetch the URLs of every photo we want to download. In the next section, we’ll use those URLs to fetch the actual image data using asynchronous Image views.