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!