Building a simple HN reader for iOS with GitHub Copilot, Part 3
In the series, I will build a simple Hacker News reader for iOS using Firebase, SwiftUI and GitHub Copilot X. In part 3, I use GitHub Copilot X to tackle the following:
- Automatically update NewsView from TopStoriesModelView
- Incorrect prompts for Firestore rabbit hole
- Successfully connect to Hacker News Firebase API
- Successfully retrieve top stories
- Use Copilot X to refactor code and add nil handling
As I mentioned at the end of Part 2’s write up, at this point I was not aware that the suggested Firestore database connection was incorrect. I ended up resolving the issues but only after a few hours of research and debugging.
1. Updating NewsView
With the app now building with a call to HN API via Firestore, I needed a way to trigger that code. As I’ve mentioned in the past, Copilot and other LLMs are non-deterministic so coding I had done while not recording didn’t always regenerate the same way.
The first time around, Copilot added a @StateObject
for the TopStoriesModileView which then triggered the init()
function when the NewsView was loaded. However, in the recorded version it switched to an @ObservedObject
. To be honest, as a new Swift programmer, I didn’t really understand the difference and I did a little research to find this helpful article:
Observed objects marked with the @StateObject property wrapper don’t get destroyed and re-instantiated at times their containing view struct redraws. Understanding this difference is essential in cases another view contains your view.
So in this simple case, I think either an Observed or a State object would have worked but I new the StateObject was what had worked previously so I use prompts to achieve the code I was looking for. Without that previous information, I’m sure I would have just accepted the ObserverObject. Anyway, here’s the prompt and generated code:
import SwiftUI
struct NewsView: View {
// create a stateobject for the TopStoriesModelView
@StateObject var topStoriesModelView = TopStoriesModelView()
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
2. Firestore rabbit hole
As I mention in my Part 2 write up, Copilot had recommended connecting to the HN Firebase API using Firestore that that was not correct. Interestingly, Copilot Chat had generally gotten the code accurate suggesting the Database connection method.
After several hours of researching and trying different solutions, I ultimately realized I needed to change the Firebase database type and I used updated prompts to generate the correct code. The initially generated code missed the url
parameter and the Chat mislabeled the parameter to fromURL
. In the end, both issues was pretty easy to resolve and here’s the resulting prompts and code:
import Foundation
import Firebase
// using Firebase, get the top 500 stories
// https://hacker-news.firebaseio.com/v0/topstories.json
class TopStoriesModelView: ObservableObject {
@Published var topStories = [Story]()
init() {
// create a new Firebase database reference to the topstories endpoint
let db = Database.database(url: "https://hacker-news.firebaseio.com/").reference().child("v0").child("topstories")
print(db)
// loop through the first 500 stories
db.queryLimited(toFirst: 500).observeSingleEvent(of: .value) { snapshot in
// loop through the snapshot children
for child in snapshot.children {
// get the child as a DataSnapshot
let snap = child as! DataSnapshot
// get the value as an Int
let value = snap.value as! Int
// create a new Firebase database reference to the story endpoint
let db = Database.database(url: "https://hacker-news.firebaseio.com/").reference().child("v0").child("item").child("\(value)")
// get the story
db.observeSingleEvent(of: .value) { snapshot in
// get the value as a dictionary
let value = snapshot.value as! [String: Any]
// create a new Story
let story = Story(id: value["id"] as! Int,
title: value["title"] as! String,
url: value["url"] as! String,
by: value["by"] as! String,
time: value["time"] as! Int,
score: value["score"] as! Int,
descendants: value["descendants"] as! Int,
kids: value["kids"] as! [Int])
self.topStories.append(story)
print(story)
}
}
}
}
}
One of the really nice parts was Copilot realized a second call to Firebase to resolve each stories detail was needed. This was very cool because I was going to work on that next but Copilot took care of it in the code immediately.
I was very excited to see HN stories finally fly by!
3. Refactor code to address nil properties
But the app did crash after looping through a number of stories and the following error was in the console:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
Since I had already taken a chunk of Angela Yu’s iOS programming class, I understand the problem. Essentially, one of the story properties was nil
but I wasn’t handling this in code. I knew I could add a bunch of conditional code but I was honestly dreading it as it is just tedious.
But one really cool capability in GitHub Copilot Chat is the ability to refactor code. So I tinkered a bit a first with prompts but then selected the code and used the /fix
command in Chat:
/fix the selected code does not check for nil and if found use a deault value. Can you help me refactor it?
Chat did a great job of writing this code and making it simple to add it to my open Swift file:
import Foundation
import Firebase
// using Firebase, get the top 500 stories
// https://hacker-news.firebaseio.com/v0/topstories.json
class TopStoriesModelView: ObservableObject {
@Published var topStories = [Story]()
init() {
// create a new Firebase database reference to the topstories endpoint
let db = Database.database(url: "https://hacker-news.firebaseio.com/").reference().child("v0").child("topstories")
print(db)
// loop through the first 500 stories
db.queryLimited(toFirst: 500).observeSingleEvent(of: .value) { snapshot in
// loop through the snapshot children
for child in snapshot.children {
// get the child as a DataSnapshot
let snap = child as! DataSnapshot
// get the value as an Int
let value = snap.value as! Int
// create a new Firebase database reference to the story endpoint
let db = Database.database(url: "https://hacker-news.firebaseio.com/").reference().child("v0").child("item").child("\(value)")
// get the story
db.observeSingleEvent(of: .value) { snapshot in
// get the value as a dictionary
let value = snapshot.value as! [String: Any]
// create a new Story
let id = value["id"] as? Int ?? 0
let title = value["title"] as? String ?? ""
let url = value["url"] as? String ?? ""
let by = value["by"] as? String ?? ""
let time = value["time"] as? Int ?? 0
let score = value["score"] as? Int ?? 0
let descendants = value["descendants"] as? Int ?? 0
let kids = value["kids"] as? [Int] ?? []
let story = Story(id: id, title: title, url: url, by: by, time: time, score: score, descendants: descendants, kids: kids) // append the story to the topStories array
self.topStories.append(story)
print(story)
}
}
}
}
}
While the code isn’t exactly what I wanted (we should really ignore a a story if key properties like the id
are missing), it worked well to handle nil
properties. After I thought about it later, I suspect there was a story with no descendants or kids yet.
4. GitHub Copilot X thoughts - part 3
While Copliot took me down a rabbit hole with using the incorrect Firebase database type (Firestore), it’s hard for me to be too upset about that. First, the documentation for the HN Firebase API is very sparse and I would say this is not a common issue. Usually a developer will have a good understanding of the type of database they are connecting to. I was also very new to Firebase so didn’t have previous experience to build on.
And in the end, Copilot Chat had actually recommended the directionally correct code! Once I tweaked the prompt to specify connecting via a Firebase Database, things went great.
And I absolutely loved the ability to refactor code with /fix
which saved me a lot of tedious boiler plate coding!
In part 4, I use Copilot Chat to make short work of some developer housekeeping that I was putting off!