I’ve spent most of my career in Developer Relations and consider myself an Apple (mostly iOS and WatchOS) dev by background. Building a Vision Pro app just made sense. And why not a stock market app? Sharing my progress in a sort of ongoing, open Dev Diary also made sense. This series isn’t going to be an end-to-end tutorial. It will be more of an experiment in progress, where I will share learnings, trial and error, and tonnes of code snippets and utilities that help other Swift developers.
Enough rambling, here we are, ready to get started…
A Swift Wrapper
I'm going to use Polygon.io to fetch market data. They have a free plan. Yay. They also have a number of client libraries that wrap the RESTful APIs. However, currently there is not one for Swift. Boo. I’ve fallen into the habit of spending too much time creating a full featured package for things like this before, but this time I really want to jump in and proof things out.
After doing some research, I also discovered Apple has a Swift-OpenAPI generator project on GitHub. Polygon provides an OpenAPI spec already. I bookmarked the Apple project and will come back to once I get the basics up and running.
Authentication
To access the APIs, you need an API key, which which you can get with the free Basic plan. Once you have it, authorization is via a Bearer token making it easy to create a simple wrapper client using Alamofire.
The first piece of data I know I am going to need is Aggregates. Generally, I’ll test the endpoint using Postman or the interactive docs just to make sure I’m seeing the payload I expect.
PolygonClient
For now, let’s create a pretty basic API client, PolygonClient.swift, to fetch data. Before we do that, we need to store the API key somewhere. I’m going to use Info.plist and add a new key named Polygon API key, which I can reference from my code.
I have to come back to this approach later and use .xcsecrets or keychain, but for now this works fine but I can't really add Info.plist to my .gitignore. Adding a todo to come back and revisit this soon.
Now I have my API key set up, I can add it to my client code
import Foundation
import Alamofire
class PolygonClient {
static let shared = PolygonClient()
private let baseURL = "https://api.polygon.io/v2"
func getAPIKey() -> String {
if let apiKey = Bundle.main.infoDictionary?["Polygon API Key"] as? String {
return apiKey
} else {
print("No Polygon API key found. Add it to Info.plist")
return ""
}
}
}
Let’s add the call to fetch aggregate information. You’ll notice I uppercase the symbol passed in to the function. Polygon is case sensitive on ticker symbols. That got me stuck for a while, despite it being clearly called out in the docs (yep, I'm a dev, I don't already read the docs...) Once I get the response from the server, I send it back via a Promise.
func fetchAggregates(symbol: String, multiplier: Int, timespan: String, from: String, to: String, sort: String, completion: @escaping (Result<StockData, Error>) -> Void) {
let headers: HTTPHeaders = [
"Authorization": "Bearer \(getAPIKey())"
]
let endpointURI = "/aggs/ticker"
//polygon is case sensitive re ticker symbols. Always uppercase, just in case
let aggsurl = baseURL + endpointURI+"/\(symbol.uppercased())/range/\(multiplier)/\(timespan)/\(from)/\(to)?sort=\(sort)"
AF.request(aggsurl, headers: headers).responseDecodable(of: StockData.self) { response in
switch response.result {
case .success(let stockData):
completion(.success(stockData))
case .failure(let error):
completion(.failure(error))
}
}
}
Strongly typed structs
Once the JSON response is returned, I’m taking advantage of Swift’s Codable protocol to dynamically map JSON elements to a strongly typed struct which I’ve creatively called StockData. It’s this struct that my app will use any time I am working with data. This decouples the API payload from my app allowing me to encapsulate changes to a single class. Right now the struct is super simple, but it is much better than peppering your app logic with JSON parsing logic which may change as a provider changes their endpoints. Creating strongly typed structs that conform to the Codable protocol is going to be really importance as I build a more fully fledged Swift wrapper for the Polygon APIs too.
import Foundation
struct StockData: Codable{
let results: [StockResult]
struct StockResult: Codable, Identifiable {
let id = UUID()
let t: Double //polygon returns a universal date format.
let c: Double
enum CodingKeys: String, CodingKey {
case t = "t"
case c = "c"
}
}
}
That all looks good for right now. Pretty simple, but it's all I need.
The VisionPro app
I’m far from an expert at VisionPro. Thankfully SwiftUI works pretty universally across different platforms from MacOS, iOS, and now VisionOS. The overall plan is to have multiple floating windows for the user to interact with. The main window will be the control pane where you enter a ticker symbol and adjust criteria like date ranges and submit your request to return the results in a chart. Then, to the side, I wanted to experiment with ancillary information like company news.
Exciting huh? It’s an iterative process. I want to use this project as a learning tool. My thought was to really take advantage of VisionPro’s use of space to add contextual information, and experiment with gestures to do some dynamic activities like zooming into a specific time period and have data retrieved based on that new slice of time. Enough talking, let’s get coding.
A basic ContentView
Everything in SwiftUI starts with a ContentView. My layout for the main screen is pretty typically too with a split view and few fields to accept user input.
import SwiftUI
import RealityKit
import RealityKitContent
import Charts
struct ContentView: View {
@State private var tickerText = ""
@State private var stockData: StockData?
var body: some View {
NavigationSplitView {
VStack {
TextField("Enter stock ticker", text: $tickerText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
submitAction()
}) {
Text("Submit")
.padding()
.foregroundColor(.white)
.cornerRadius(10)
}
}
.navigationTitle("Polygon.io Demo")
} detail: {
VStack {
//my chart will go here
}
.navigationTitle("Stock Information")
.padding(10)
.onAppear {
//do something
}
}
}
Handling button clicks and fetching data
So far so good. The sidebar let’s the user add a ticker symbol, and I hooked up a button action to submitAction() func. This is where I will call the PolygonClient we created earlier. Let’s implement this action now.
func submitAction() {
fetchAggregates(symbol: tickerText)
}
private func fetchAggregates(symbol: String) {
print("Fetching aggregates for "+symbol)
PolygonClient.shared.fetchAggregates(symbol: symbol, multiplier: 1, timespan: "month", from: "2022-06-01", to: "2024-06-05", sort: "asc") { Result in
switch Result {
case .success(let data):
DispatchQueue.main.async {
self.stockData = data
}
case .failure(let error):
print("Error fetching stock data: \(error)")
}
}
}
Generally, I try to add my actual logic separate from say a button click func. This way if I need to call the logic from somewhere else in my code, it is not tied to the UI at all. In this instance, the submitAction which is called from the button simply calls fetchAggregate, which does all the heavy lifting.
Here’s where I fetch the data and wait for the promise to return with the payload. Once I have the data, I need to map it to a chart. That’s what I’ll work on next.
Visualize the data in a bar chart
I am going to visualize the data in a pretty simple bar chart which will appear in the detail section of my split view. I already added a VStack placeholder.All I need to do is loop through the results and create a series of Bar Marks, or points, for the chart.
if let stockData = stockData {
Chart {
ForEach(stockData.results) { dataPoint in
BarMark(
x: .value("X", dataPoint.t),
y: .value("Y", dataPoint.c)
)
}
}
.chartYAxis {
AxisMarks(values: .stride(by: 20)) {
AxisValueLabel(format: Decimal.FormatStyle.Currency(code: "USD"))
}
}
} else {
Text("Enter a stocker ticker")
}
Looking a little wonky
If you run the app now, everything should be working great in regards to retrieving the aggregate information, but the chart looks wrong. The problem is the X axis. It looks kinda wonky. Yep, that's a technical term... The reason things look odd is that Polygon returns data with unix timestamps in milliseconds. We need to convert it to a date we can work with before binding it to the chart.
Jump back to the PolygonClient wrapper and the following helper func.
// Polygon returns a unix timestamp in milliseconds. We need to convert this to a date before working with it.
func convertUnixTimeToDateString(unixTime: Double) -> String {
let date = Date(timeIntervalSince1970: unixTime / 1000)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM" // Specify desired date format
let dateString = dateFormatter.string(from: date)
return dateString
}
Then, I can update BarMark point for the x axis like this.
BarMark(
x: .value("X", PolygonClient.shared.convertUnixTimeToDateString(unixTime: dataPoint.t)),
y: .value("Y", dataPoint.c)
)
Take two. Dewonkified!
Running the app again, everything looks much better!
Wrapping up
That’s a pretty good start. I got my client wrapper working using the Polygon.io free plan and mapping to a struct, plus created ,an albeit simple, interface to show aggregate data. I’m a long way from done, but the bones are there. Next, I’ll look at incorporating some gestures like changing date ranges via zooming, and add the company info news feed. If you want to follow along, make sure you star and watch the GitHub repo.
Top comments (0)