Jacob’s Tech Tavern

Jacob’s Tech Tavern

My Toddler Still Loves Planes, So I Upgraded Her Radar

An adorable set of release notes

Jacob Bartlett's avatar
Jacob Bartlett
Jan 22, 2024

This is part indie dev story, part release notes, and part technical documentation as I detail the long-awaited upgrade to Aviator — Radar on your Phone! 

Late-night plane-spotting using the newly upgraded Aviator 2.0

My best ever story

My Toddler Loves Planes, So I Built Her A Radar was by far the best thing I’ve written. I managed to hit that sweet spot of wholesome and technical, and my sheer enjoyment of the project shined through in my narrative.

Posting a week after the OpenAI drama, I was *this* close to making the top post of the week, but was just beaten out by the late, great Charlie Munger

One would think, as an enterprising indie developer, I’d jump straight on top of the hype train, shoot out another release, and begin dreaming up monetisation opportunities. 

But there was a stumbling block! Unfortunately, Aviator had been broken since early December. OpenSky Network had been having an incident which gave all my users an error.

A power outage in the open-source OpenSky Network 

After trying very hard not to think about this for several weeks, a miracle happened. I opened the app again, expecting to torture myself with the persistent 502 error message, when I heard the characteristic beep-boop of planes detected on the radar.

It was working again!

Newly energised by this belated Christmas miracle, I dove headfirst into two intense evenings* of overhauling Aviator with the ideas that had been brewing in the back of my brain.

*After getting my toddler to bed, of course.

Introducing Aviator 2.0 

This release includes several shiny new features, and solves several problems my daughter and I experienced while we played with the original.

New UI 

I’ve overhauled the UI so the controls now live in a separate menu. This utilises progressive disclosure to keep the major controls front-and-centre, and the advanced, less frequently accessed tools below the fold.

Yes, picking a colour is absolutely one of the 3 core controls. My daughter likes it more than the planes.

This menu is implemented as a modal with two presentation detents:

  • primary which takes up 0.15 of the screen’s height

  • extended which takes up 0.4 of the screen’s height

It also has a presentation drag indicator so users know they can extend the controls menu, and vitally enables background interaction so we don’t cover the main radar UI in shadow.

// ControlsView.swift

var body: some View {
    // ... 
    .presentationDetents(availableDetents, selection: $selectedDetent)
    .presentationDragIndicator(.visible)
    .presentationCornerRadius(24)
    .presentationBackground {
        LinearGradient(colors: [.gray, .spaceGrey],
                           startPoint: .top,
                           endPoint: .bottom)
                .padding(-2)
                .embossEffect()
        }
    .presentationBackgroundInteraction(.enabled)

I’m planning to utilise the new TipKit in 2.1 to ensure this progressive disclosure isn’t lost on our younger, less experienced Apple fans.

This presentationBackground wraps up another piece of polish: I used Paul Hudson’s emboss effect. This is another Metal shader which applies a lovely metallic texture to the menu, further improving on the skeuomorphism of a control panel for a real-life radar.

If you want to learn more about making your own shaders in SwiftUI, read my tutorial Metal in SwiftUI: How to Write Shaders.

Zoom levels

While building 1.0, I experienced a persistent problem. The radar was too damn good. 

More specifically, it was capturing flight details of aircraft I didn’t have a hope of seeing, either far beyond the horizon or hidden behind terrain. This became a real pain-point in the terraced archipelago of suburban London. 

This wasn’t just a sensitivity issue, however — when I went out to a lovely open space, I could see planes for miles and miles. 

The solution was clear: implement zoom. 

Aviator’s view over London, default (left) and zoomed in (right)

The MapKit SDK has the concept of a camera which hovers above the planet, showing your map. You can feed it a distance (in metres) and it zooms in and out of the Earth’s surface based on this.

// AviatorView.swift

@AppStorage("zoomed") var zoomed: Bool = false

// ...

cameraPosition = .camera(MapCamera(centerCoordinate: coordinate,
                                   distance: (zoomed ? 70 : 100) * 1_000,
                                   heading: angle))

I also, in my rush to get a competent release out over the weekend, simply removed one of the radar circles to emphasise the zoomed-in UI.

// RadarView.swift 

private var radarCircles: some View {
    GeometryReader { geometry in
        let diameter = min(geometry.size.width, geometry.size.height)
        let middleDiameter = diameter * (2.0 / 3.0)
        let innerDiameter = diameter * (1.0 / 3.0)
        
        ForEach(zoomed ? [diameter, middleDiameter]
                       : [diameter, middleDiameter, innerDiameter], id: \.self) {

        // drawing the radar circles ... 
}

Originally, I had a Slider component which handled zoom levels, however even my nifty A17 chip couldn’t quite handle this at a reasonable frame-rate. I’m using a binary zoom toggle for now, but might brainstorm alternative ways to handle this interface.

Flight information

The OpenSky Network API gathers a wealth of information from aircraft transponders, which I was not taking advantage of in 1.0. These helped feed into many of the new features in 2.0.

Foremost of these is the ability to toggle flags — displaying the countries of origin for each plane, as a simple emoji underneath each icon.

Toggling origin flags on and off

This utilised the origin_country property from the OpenSky Network API. This gave the plane start point as a straightforward text string. Using a comprehensive list of country names found online, I could convert the string into a 2-character country code, turn this into unicode scalars, and ultimately construct a flag emoji.

// Country.swift

extension Flight {
    
    var flag: String {
        guard let countryCode = Country(name: origin_country)?.abbreviation else { return "" }
        return countryCode.uppercased().unicodeScalars.reduce(into: "") {
            if let scalar = UnicodeScalar(UInt32(127397) + $1.value) {
                $0.unicodeScalars.append(scalar)
            }
        }
    }
}

It was then simple to add text to the MapKit annotation when the user toggled this feature on.

@AppStorage("showFlags") var showFlags: Bool = false

private var planeMapAnnotations: some MapContent {
    ForEach(flights.filter, id: \.icao24) { flight in
        Annotation(showFlags ? flight.flag : "", coordinate: flight.coordinate) {
            // custom annotation view ... 
        }
    }
}

Subscribe for free to continue reading

Enter your email and get every post from Jacob’s Tech Tavern.
Already have an account? Sign in
© 2026 Jacob Bartlett · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture