Building Hunt Coordinate Converter: CI/CD Infrastructure

When I set out to build Hunt Coordinate Converter, I wanted an automated pipeline that would let me develop features quickly without worrying about manual releases. This post covers the decisions and lessons learned from setting up continuous delivery for the app.

The Challenge

As a solo developer with a full-time job, I needed a system that:

  1. Validated every change automatically
  2. Got new features into TestFlight quickly
  3. Handled version numbers without manual work
  4. Made releases low-friction

Branching Strategy

I settled on a simple flow:

feature/* ──► TestFlight (Internal) ──► Main ──► TestFlight (External) ──► App Store

Feature branches trigger automatic builds to internal testers. Once approved, merging to main kicks off an external TestFlight build. When I'm ready, I submit to the App Store.

Versioning Without the Headache

Keeping version numbers consistent across builds is tedious. I automated it: the build script reads the git tag, counts commits, and generates a unique version number. Every build gets a unique identifier without me touching anything.

Two Workflows

Feature Workflow handles feature/* branches—runs tests, builds, sends to internal TestFlight on every push.

Production Workflow watches main—runs the full suite, then uploads to external TestFlight for broader testing before App Store release.

Storage Architecture

For the history feature, I chose a simple pattern:

  • Local-first: Data saves immediately to the device
  • Opt-in sync: iCloud only activates when you turn it on
  • No external APIs: Everything stays on your device

This avoids surprises for users who don't want cloud features.

iCloud Sync Details

The iCloud key-value store is convenient but has quirks:

  • Requires an iCloud account (obviously)
  • Sync happens in the background, not instantly
  • Can fail silently if the network is spotty

To avoid errors on launch for users who don't want sync, I only initialize the iCloud connection when the user explicitly enables it in settings.

Key Decisions

ObservableObject for Shared State

I used this pattern for managing history because it integrates naturally with SwiftUI's view system and persists well to local storage.

Deferring iCloud Initialization

Starting cloud features on app launch can cause errors if the user isn't signed into iCloud or has spotty connectivity. By waiting until the user opts in, I avoid cluttering the logs with failures that don't matter to most users.


For more details on the app itself, see my Hunt Coordinate Converter announcement.