Intro
Together with
Sioux, we built the Coach-Copilot app for
Equestic.
On the iOS side, I led the architectural direction and was
responsible for how the mobile system was designed and
implemented. That included defining the app structure, shaping
shared state and navigation, designing the networking and
authentication layers, handling lifecycle-aware behavior, and
building the audio recording and upload workflows so they could
hold up under real-world mobile conditions.
What made the project especially interesting was that it
required much more than screen implementation. Coach-Copilot had
to support authenticated session flows, push and deep-link entry
points, lifecycle-heavy behavior, and durable audio workflows.
From the mobile perspective, that pushed the work firmly into
systems design rather than straightforward feature delivery.
Context
Coach-Copilot is an AI powered production mobile application
built with SwiftUI on the iOS side in the equestrian world that
helps coaching sessions be more structured, actionable, and
continuous by capturing training context and turning it into
useful follow-up insights.
The app had to support more than a simple linear user journey.
It needed to coordinate state across:
- authenticated and unauthenticated entry
-
lifecycle transitions between foreground, background, and
inactive states
- push-triggered and deep-link-triggered flows
- asynchronous session updates
- audio recording, persistence, and upload
- environment-specific runtime behavior
- production monitoring and diagnostics
Because the product was being built collaboratively across
organizations, the iOS side needed clear architecture, strong
boundaries, and predictable behavior. My responsibility was to
ensure the mobile system could evolve cleanly while remaining
reliable under operational complexity.
The Business Problem
A large part of equestrian coaching happens in fast-moving,
high-context situations.
Important information is often communicated verbally during a
training session:
- observations about the horse
- corrections for the rider
- coaching decisions made in the moment
- exercises that worked or failed
- patterns that should be revisited
- actions to take during the next session
Without a reliable digital workflow, much of this context
remains ephemeral. It can be difficult to preserve consistently,
revisit later, or transform into structured insight.
Coach-Copilot addresses that problem by creating continuity
between the live training moment and what happens afterward.
The product helps:
- coaches capture and preserve session context
-
riders revisit coaching feedback and training observations
-
individual users reflect on solo rides and groundwork sessions
-
training workflows become more consistent across activities
-
AI-assisted services transform captured information into
structured summaries
-
coaches and riders maintain clearer follow-up between sessions
The reliability of the mobile implementation is directly
connected to that value. If recording is fragile, uploads fail
silently, or activity state becomes inconsistent after
backgrounding, the product cannot deliver on its promise.
My Role
I worked as an external iOS contractor alongside
Sioux
to deliver the application for
Equestic.
On the iOS side, I led the architectural direction and
implemented the foundations behind the product. My
responsibilities included:
- defining the overall iOS architecture
-
structuring the application by feature and responsibility
- designing shared observable state
- building the SwiftUI navigation system
- defining service and repository boundaries
- composing production and stub dependency graphs
- designing authenticated request execution
- coordinating concurrent token refresh
- centralizing lifecycle and system-event behavior
- implementing audio capture and interruption recovery
-
designing local audio persistence and recording recovery state
- building the background upload workflow
-
supporting coaching and individual-reflection activities
-
integrating summaries, memberships, telemetry, and runtime
configuration
- making critical layers independently testable
In practice, this meant combining technical leadership with
hands-on implementation. The goal was not only to deliver
features, but to create an iOS system that could continue
evolving without losing predictability.
Current Product Scope
The current application has grown beyond a single
coaching-session flow.
It includes:
- authentication, OTP verification, and guided onboarding
- referral-aware registration and deep-link entry
- coach, rider, buddy, user-profile, and horse management
- live coaching sessions
- solo-ride reflections
- groundwork reflections
- activity-specific setup workflows
- audio capture and local persistence
- background-capable upload and recovery
- structured summaries with editing and approval
- activity history, search, and in-progress restoration
- QR and referral-based coaching invitations
- SaddleClip account linking
- news and community updates
- Subscription-backed memberships and purchases
- push notifications and universal links
- network reachability and remote configuration
- analytics, telemetry, and crash reporting
- production and backend-independent stub environments
This broader activity model is one of the most important
evolutions from the original implementation.
Engineering Goals
I optimized the iOS architecture around five goals.
1. Explicit system boundaries
The application includes authentication, activities, audio,
uploads, summaries, memberships, notifications, and external
entry points. These concerns needed clear ownership so that
changes in one area would not destabilize unrelated features.
2. Reliability under real mobile conditions
The app needed predictable behavior during:
- network degradation
- token expiration
- audio interruptions
- input-route changes
- background transitions
- local finalization
- upload retries
- app relaunches
- push and deep-link entry
- partially completed activities
3. Extensible activity workflows
Live coaching, solo rides, and groundwork reflections share some
concepts but require different participants, steps, permissions,
and completion behavior.
The architecture needed to support those differences without
duplicating an entire feature for every new activity type.
4. Operational visibility
Failures involving authentication, recording, finalization, or
uploads are difficult to diagnose from a screenshot. Logging,
telemetry, and crash reporting therefore had to be part of the
architecture rather than added after release.
5. Testability
Routing, repositories, services, onboarding, activity state, and
presentation logic needed to be isolated behind replaceable
dependencies.
Testability was treated as feedback on the design itself.
Technical Baseline
The current application is built with:
- SwiftUI
- the Observation framework and
@Observable
-
Swift concurrency with
async/await
and actors
NavigationStack and typed route values
- AVFoundation for recording
- background
URLSession uploads
- Swift Package Manager through Xcode
- an iOS 18+ deployment baseline
- Swift 5 language mode
- an Xcode 26.2-verified toolchain
The application targets iPhone and uses a portrait-oriented
interface.
The current project does not claim a completed Swift 6
strict-concurrency migration. Instead, concurrency-sensitive
subsystems use explicit actor isolation, while UI and lifecycle
coordination remain main-actor-bound.
Application Architecture
I structured CoachCopilot as a pragmatic layered
SwiftUI application:
- SwiftUI views
- Observable view models and workflow state
- Feature services
- Repositories and stores
-
HTTP execution, Keychain, UserDefaults, and file storage
The main responsibilities are separated as follows:
- Views render state and forward user intent.
-
View models manage presentation logic and screen-level
orchestration.
- Workflow types define multi-step activity behavior.
- Services coordinate feature-level operations.
-
Repositories isolate backend requests and persistence
boundaries.
-
Stores own tokens, files, upload jobs, and other durable data.
-
Shared observable state coordinates application-wide
information.
-
Dependency injection assembles production or stub
implementations.
-
System components own startup, lifecycle, push notifications,
links, background-session bridging, and telemetry.
This keeps SwiftUI screens focused on presentation while moving
business and infrastructure behavior into independently
understandable layers.
Dependency Composition and Shared State
Application composition happens at the app boundary.
At launch, CoachCopilotApp:
- loads the configuration for the selected environment
- configures third-party integrations
- constructs stores and repositories
- assembles either real or stub services
- creates the dependency container
- injects that container into SwiftUI
-
connects lifecycle, push, deep-link, connectivity, and upload
handling
This avoids constructing service graphs inside individual
screens.
The root AppState uses Observation and groups
cross-cutting state into focused sub-states, including:
- authentication
- permissions
- connectivity
- notifications
- active activity and session information
- user data
- UI state
- deep-link data
- membership state
- force-update requirements
Navigation is owned separately by an observable,
main-actor-isolated router. This prevents backend state and
navigation-stack mutations from becoming the same concern.
The result is centralized coordination without turning every
feature into a dependency on one monolithic object.
An Extensible Activity Model
One of the most significant architectural developments is the
unified activity system.
The application currently supports three primary activity types:
- live coaching sessions
- solo-ride reflections
- groundwork reflections
These activities share lifecycle concepts such as creation,
recording, finalization, upload, and summary processing, but
their setup requirements differ.
Rather than hard-coding one large conditional screen, the app
models activity creation through an
ActivityWorkflowEngine. Each activity defines:
- its availability rules
- supported training specifications
- an ordered list of setup steps
- its completion action
- whether rider creation is available
- whether it is an individual-reflection activity
For example, a live coaching session follows a flow similar to:
- choose a training specification
- select a rider
- select a horse
- review the selections
- send the coaching invitation
A solo ride or groundwork reflection follows a different flow:
- choose a training specification
- select a horse
- choose the activity date and time
- review the selections
- confirm the individual activity
Activity availability is also explicit. Live coaching is
coach-specific, while individual reflection activities are
available to a broader user group.
Supported specifications currently span riding and groundwork
contexts such as dressage, show jumping, hunter, cross-country,
trail riding, lunging, in-hand training, lead-rope training, and
liberty training.
This design allows each activity to evolve independently while
reusing the shared presentation and lifecycle infrastructure.
Activity Lifecycle Coordination
The application does not treat recording as an isolated media
action. It is part of a larger backend activity lifecycle.
A shared activity lifecycle service provides operations for:
- cancelling an activity
- marking recording as started
- finalizing recording metadata
- marking uploaded audio as complete
The service routes those operations to the appropriate
coaching-session or individual-reflection implementation.
This allows the recording and upload layers to work with a
consistent contract without embedding activity-specific backend
logic inside the audio subsystem.
The activity domain also supports:
- activity history
- activity search
- backend state restoration
- detection of locally in-progress activity information
- coaching invitation and referral states
- summary-ready and summary-approved states
This is more robust than treating each screen transition as an
independent callback. The app and backend cooperate around an
explicit activity lifecycle.
Typed Navigation and Multi-Entry Flows
Navigation is implemented through an observable router and typed
route values.
Routes cover flows such as:
- login and registration
- OTP verification
- onboarding
- home
- activity creation
- recording
- summary review
- account management
- rider and coach management
- horse management
- memberships
- external account linking
The root navigation layer decides whether the authenticated or
unauthenticated application hierarchy should be visible, while
the router manages the current NavigationPath.
External signals are passed into this system through centralized
app state and lifecycle handling. These signals include:
- universal links
- custom URLs
- referral links
- push-notification interactions
- restored activity state
This separation makes it possible to accept external entry
points without distributing routing logic across unrelated
views.
Lifecycle Orchestration as a System Responsibility
Lifecycle behavior is centralized through
SystemEventsHandler, with
AppDelegate acting as the bridge to UIKit and
operating-system callbacks.
This layer coordinates:
- launch-time bootstrap
- authentication revalidation
-
active, inactive, foreground, and background transitions
- push-notification configuration
- APNs token registration and backend synchronization
- universal-link and deep-link handling
- remote configuration refresh
- API compatibility and force-update evaluation
- recording and audio-monitoring policy
- background upload completion events
- lifecycle telemetry
Push setup is intentionally separated from the user-facing
permission request. The app can configure notification handling
and reuse an existing authorization state without unexpectedly
displaying the system prompt during launch.
Device-token synchronization is also conditional on
authentication, user availability, and the presence of a stored
APNs token.
Centralizing these policies reduces the chance that multiple
screens will respond differently to the same operating-system
event.
Authenticated Networking and Refresh Coordination
The networking stack is divided into:
- typed endpoint definitions
- repositories
- a shared HTTP client
- request signing
- token persistence
- refresh coordination
- request and failure logging
Authentication tokens are stored through Keychain-backed storage
rather than in view state.
The most important concurrency concern is token refresh.
When several authenticated requests receive
401 responses at approximately the same time, a
naive implementation can start several refresh requests. That
creates race conditions around token replacement and can cause
requests to retry with inconsistent credentials.
TokenRefreshCoordinator solves this as an
actor-isolated coordination problem.
The first failing request starts refresh work. Other callers
wait through checked continuations rather than starting
additional refresh operations. After a successful refresh, the
new credentials are saved and all waiting requests can continue.
If the refresh token itself is rejected, the stored credentials
are cleared and all waiters receive the same terminal
authentication failure.
This gives the app one coordinated recovery path instead of
requiring every repository or feature to invent its own
behavior.
Audio Capture as a Systems Problem
The audio workflow remains one of the most technically demanding
parts of the application.
The recording service uses AVFoundation and handles more than
starting and stopping a file. It coordinates:
AVAudioSession configuration
- microphone input validation
- Bluetooth hands-free routes
- audio-engine and tap installation
- recording and level monitoring
- elapsed-time tracking
- interruption handling
- route-change recovery
- media-services reset recovery
- background visual-update policy
- local file output
- telemetry and failure reporting
The audio callback performs only the necessary signal work
before crossing to the main actor for file and presentation
updates. Equalizer and level updates are throttled so that
SwiftUI is not invalidated for every captured audio buffer.
When the app moves out of the active state, visual updates can
be disabled while the recording policy remains controlled
separately. This avoids unnecessary UI work while the
application is backgrounded.
The service also listens directly for
AVAudioSession interruptions rather than using
foreground transitions as a proxy for phone calls or other
system audio events.
During recovery, it can:
- pause the engine
- rebuild the engine and tap
- reactivate the audio session
- account for an interruption gap
- resume recording or monitoring when iOS allows it
Route changes and media-services resets trigger dedicated
recovery paths because the input format or audio engine may no
longer be valid after those events.
Durable Local Recording State
The latest evolution of the audio architecture introduces an
explicit local recording journal through
LocalRecordingSession.
This model is deliberately independent from the backend
ActivitySessionState. The two state machines solve
different problems:
- backend activity state describes the business workflow
-
local recording state describes the recoverability and
ownership of audio on the device
The local recording lifecycle distinguishes between:
- active recording
- local finalization
- availability of a finalized local asset
- durable upload handoff
- complete backend acknowledgement
- explicit discard or recoverable failure
A stable localFileId is created once and remains
unchanged across finalization attempts, upload retries, and
application relaunches. This gives the recording, local asset,
and upload saga a shared durable identity.
The journal also records:
- an immutable snapshot of the associated activity
- persisted elapsed time
- pending explicit silence
- ordered recording segments
- the finalized
AudioAsset identity
- the durable
UploadJob identity
- the most recent local-processing error
This design makes recovery decisions explicit. A session can
indicate that it still requires reconciliation without implying
that every recovery action means restarting the microphone.
Depending on its lifecycle, recovery may instead involve
validating segments, retrying finalization, rebuilding the
upload handoff, or observing an existing upload job.
Segmented Recording and Continuity
The durable recording model represents capture as a sequence of
short, independently recoverable compressed segments.
Each LocalRecordingSegment has:
- a stable identifier and sequence index
- immutable file naming
- start and end timestamps
- validated duration and byte size
- sample rate and channel count
- successfully written frame count
- explicit silence to insert before the segment
- a durable segment state
A closed segment is treated as immutable. After an interruption,
route change, crash-recovery event, or normal rotation point,
recording continues into a new segment instead of attempting to
append to a previously closed file.
This is important because audio format can change after a route
transition. Persisting format information per segment allows
later finalization to reason about those differences instead of
assuming that the whole recording used one unchanged input
format.
Segment state explicitly distinguishes files that are:
- currently recording
- safely closed
- missing from disk
- present but corrupted
- intentionally deleted
Continuity gaps are also modeled explicitly. Silence associated
with an interruption or recovery period is attached to the next
segment. The finalization layer must not infer silence from
arbitrary wall-clock differences, which could otherwise convert
unrelated timing noise into fabricated audio duration.
The new journal establishes the persistence contract and
recovery semantics required for crash-resilient capture.
Recovery execution can then be integrated around explicit state
rather than reconstructed from temporary URLs or UI memory.
Safe Finalization and Ownership Handoff
The transition from local recording to background upload is
treated as an ownership boundary.
The local journal remains responsible until:
- closed segments have been validated
-
local stitching or export has produced a durable
AudioAsset
- the resulting asset identity has been persisted
- a durable upload job has been created
- that upload-job identity has also been persisted
Only after both the finalized asset and upload job exist can raw
segments become eligible for deletion. The journal itself
remains relevant until the complete upload saga reaches its
terminal completed state.
This ordering prevents a dangerous class of failures where raw
audio is deleted after export but before durable upload
ownership has been established.
It also makes every transition recoverable by identity rather
than relying on volatile in-memory URLs.
Local Audio Persistence
Finalized audio assets are managed through an actor-isolated
local file store.
The store owns:
- temporary recording URLs
- application-support storage
- asset import
- metadata encoding and decoding
- local file identity
- lookup and persistence operations
Using an actor for this boundary prevents concurrent recording,
upload, and restoration operations from mutating the local file
library inconsistently.
It also separates AVFoundation concerns from storage policy. The
recording pipeline produces audio, while the local persistence
layer determines how that audio becomes a durable asset.
Durable Upload Coordination
Uploads are modeled as a persisted workflow rather than a single
HTTP request.
Each upload job progresses through three logical stages:
- notify the backend that recording has been finalized
- transfer the audio blob
- acknowledge that the upload workflow has completed
This distinction is critical. A successful file transfer does
not necessarily mean the business workflow is complete.
AudioUploadService is implemented as an actor and
owns:
- upload-job state
- activity type and identity
- saga phase
- deduplication
- retry state
- progress
- cancellation
- persistence
- backend acknowledgement
- telemetry
Enqueueing is idempotent for the same activity and local file
identity. If a matching active job already exists, the existing
job is reused rather than creating a competing upload.
Failures use persisted retry information and staged backoff.
Users can also trigger an immediate retry when a job remains
eligible.
The transfer itself uses a background
URLSession configured to:
- wait for connectivity
- send launch events
- use a stable background identifier
- continue independently of the foreground UI
- report progress
- rebind existing tasks after relaunch
A stable job identifier is associated with each transfer so
restored background tasks can be connected to their persisted
upload records.
AppDelegate forwards background-session completion
handling through a dedicated bridge, allowing iOS to wake the
app and finish background URL session events correctly.
This architecture separates:
- recording-journal durability
- finalized-asset durability
- transfer durability
- retry policy
- backend workflow state
- UI progress presentation
That separation is what turns audio capture and upload from one
fragile chain into a set of recoverable ownership transitions.
AI-Assisted Summary Workflows
The iOS application supports the product’s AI-generated summary
lifecycle without coupling the UI to how the backend produces
those summaries.
The summary domain supports:
- retrieving an initial structured summary
- presenting multiple summary sections
- editing section content
- saving user changes
- approving the completed summary
- retrieving approved summaries later
The same overall review model supports coaching sessions and
individual reflection activities.
The responsibility of the iOS app is not to perform the AI
processing locally. Its role is to model the asynchronous
product workflow around that processing:
- captured audio is finalized and uploaded
- backend processing progresses
- a structured summary becomes available
- the user reviews and edits it
- the final summary is approved
This keeps server-side generation separate from mobile
presentation and approval state.
Referrals, Invitations, and External Account Linking
Live coaching involves more than selecting a rider from a local
list.
The application includes referral and invitation flows that
support:
- generating coaching referrals
- QR-based invitations
- referral expiration handling
- deep-link entry
- accepting a coaching session
- passing referral information through onboarding
- restoring the intended flow after authentication
Referral data is consumed deliberately from shared state so that
it does not repeatedly trigger navigation after it has been
handled.
The user domain also supports linking a SaddleClip account. That
integration is exposed through account management and can
participate in coaching-session acceptance where the user
chooses to include the relevant data.
This demonstrates why onboarding, links, authentication, and
activity state cannot be designed independently. They are
different entry points into the same product workflow.
Memberships and Payments
Membership behavior is implemented through service and
repository boundaries backed by a third party.
The payment domain supports:
- membership information
- free-trial state
- subscription state
- subscription customer-information synchronization
- membership display information
- purchase and paywall presentation
- customer-state refresh
- backend membership synchronization
- subscription-specific logout behavior
Membership state is represented separately in the shared
application state rather than being inferred independently by
each screen.
This gives the product one place to coordinate backend
membership information with subscriptions customer state.
Runtime Configuration and Environments
The project contains four application schemes.
Each target selects its own runtime configuration.
Configurable behavior includes:
- backend base URL
- API client identity
- supported API versions
- real versus stub dependency graphs
- launch bootstrap behavior
- active-state revalidation
- background-recording policy
- subscription configuration
The API compatibility configuration is used alongside remote
configuration to evaluate whether the current app version
remains supported and whether a forced update is required.
Stub services can replace backend-backed implementations without
changing the UI contracts. This improves local development and
allows feature work to proceed without depending on every remote
environment being available.
Observability and Production Diagnostics
Production behavior is observable through several complementary
systems:
- Third party analytics
- structured application logging
- scoped networking logs
- audio and upload telemetry
- lifecycle breadcrumbs
Analytics is integrated into network and system failure paths.
For long-running workflows, telemetry includes more than a final
error. Breadcrumbs and state snapshots make it possible to
understand which stage failed and what the application believed
the current activity, recording, or upload state to be.
That is especially valuable for failures that depend on timing,
background execution, or operating-system behavior.
Testability
The current test target contains:
- 29
*Tests.swift test files
- 22 Swift mock, spy, stub, and support files
- 51 Swift files in total
Coverage includes:
- authentication repositories and services
- activity repositories and services
- coaching-session behavior
- rider, buddy, horse, user, and payment boundaries
- Keychain storage
- shared activity and membership state
- app-delegate integration
- typed routing
- onboarding and OTP presentation logic
- recording and summary view models
- activity-list presentation
- networking repository behavior
Mocks and spies are organized alongside the test target so
services, repositories, routers, and system behavior can be
replaced independently.
The production dependency graph also has a parallel stub graph,
which supports backend-independent development in addition to
automated testing.
The architectural value of these tests is not only regression
protection. They also confirm that important responsibilities
can be isolated without bootstrapping the entire application.
The durable segmented-recording journal is a newer architectural
area. Its recovery and persistence invariants are intentionally
explicit so dedicated tests can exercise schema compatibility,
segment validation, interruption continuity, finalization
retries, upload handoff, and cleanup safety as the production
integration evolves.
Key Technical Tradeoffs
Explicit layering over direct feature coupling
The architecture introduces more types than placing API calls
directly inside view models. That additional structure was
intentional because the product’s complexity comes from
interactions between systems, not from the number of screens.
A unified activity abstraction without erasing differences
Coaching, solo rides, and groundwork share lifecycle behavior,
but they do not have identical participants or setup steps.
Shared contracts are used where the behaviors genuinely align,
while each activity still defines its own workflow.
Separate local and backend state machines
The backend activity lifecycle and local recording lifecycle are
deliberately independent. Combining them would make device-level
recovery depend on remote state and would make backend
transitions responsible for local file ownership.
Keeping them separate allows reconciliation at explicit
boundaries.
SwiftUI with targeted UIKit integration
The product interface is built with SwiftUI. UIKit remains where
the operating system requires it, particularly app-delegate
callbacks, remote notifications, and background URL session
integration.
Actor isolation for mutable concurrent subsystems
Actors are used around token refresh, local audio storage, and
upload coordination because these components own mutable state
accessed by multiple asynchronous callers.
UI, routing, lifecycle coordination, and presentation state
remain main-actor-isolated.
Immutable segments over appending after disruption
Continuing into a new segment after an interruption or
input-format change creates more finalization work, but it
avoids trusting a file that may have been incompletely closed or
created with a different format.
Explicit silence over inferred timing gaps
Persisting known continuity gaps requires additional state, but
it prevents arbitrary wall-clock differences from being
converted into incorrect audio duration.
Safe ownership transfer over early cleanup
Keeping raw segments until both the finalized asset and upload
job exist consumes more temporary storage, but it avoids
irrecoverable loss between local export and durable upload
ownership.
Operational readiness over invisible failure
Telemetry and persisted workflow state add implementation cost,
but they substantially improve the ability to recover from and
diagnose production failures.
Results
The result is an iOS application that operates as a coordinated
production system rather than a collection of independent
screens.
The architecture provides:
-
one extensible model for different equestrian activities
-
clear boundaries between presentation, workflows, services,
and I/O
-
coordinated recovery from concurrent authentication failures
- interruption-aware audio capture
-
an explicit local recording journal and segmented-recovery
model
-
safe handoff from raw capture to finalized assets and upload
jobs
- persisted and resumable upload jobs
- distinct transport and business-completion states
-
typed routing across authentication and external entry points
- environment-aware runtime composition
- backend-independent development through stubs
- production diagnostics for lifecycle-heavy workflows
- testable critical layers
Most importantly, these technical foundations support the
product’s actual value: preserving real training context and
turning it into structured information users can revisit and act
on.
What I’m Most Proud Of
The strongest part of CoachCopilot is not one
isolated feature.
It is the way the iOS system connects:
- real-world audio capture
- activity-specific workflows
- authentication
- local persistence
- recording recovery state
- background transfer
- backend lifecycle state
- AI-assisted summaries
- navigation
- subscriptions
- operational telemetry
I am particularly proud of:
- leading the iOS architectural direction
-
evolving the application toward a unified activity model
- designing the audio workflow as a complete subsystem
-
separating local recording state from backend activity state
-
modeling capture, finalization, and upload as durable
ownership transitions
-
separating file transfer from logical workflow completion
- centralizing lifecycle and multi-entry behavior
- treating token refresh as a concurrency problem
- preserving clear production and stub dependency graphs
-
keeping critical services and presentation logic testable
That combination represents the kind of engineering work I value
most: taking ownership of a complex mobile system and making its
behavior explicit, recoverable, and maintainable.
Key Takeaways
Product reliability begins below the UI
For a product based on capturing real-world activity, recording,
persistence, finalization, upload, and restoration are part of
the user experience even when they are invisible.
Shared workflows should model genuine common behavior
The activity abstraction works because it shares lifecycle
concepts without pretending that coaching and individual
reflection have identical requirements.
Local and remote state solve different problems
A backend activity and a device recording can progress
independently. Modeling them separately makes recovery and
reconciliation explicit instead of accidental.
Recovery needs durable identity
A stable local file identity, finalized asset identity, and
upload-job identity allow the system to reconstruct ownership
without depending on volatile memory or temporary URLs.
Foregrounding, backgrounding, notifications, links,
interruptions, route changes, and relaunches should be modeled
as inputs into the architecture rather than scattered callbacks.
Authentication recovery is a concurrency concern
Coordinating refresh centrally prevents duplicated work and
inconsistent token state when several requests fail together.
Transport completion is not business completion
Durable workflows need to distinguish between uploading bytes
and completing the backend process associated with those bytes.
SwiftUI benefits from disciplined surrounding architecture
SwiftUI provides an effective presentation layer, but production
resilience still depends on dependency boundaries, concurrency
control, lifecycle design, persistence, and observability.
Closing
Equestic
Coach-Copilot is one of the clearest examples of the engineering
work I enjoy most: building a production mobile system where
architecture directly supports product value.
The project was delivered for
Equestic
through a collaboration between me as an external iOS contractor
and
Sioux. On the iOS side, I led the architectural direction and
implemented the foundations behind the application.
What makes the project meaningful is the connection between
technical depth and real-world use. The architecture is not
complex for its own sake. It exists so coaches and riders can
reliably capture training moments, preserve their context, and
turn them into structured, actionable follow-up through an
AI-assisted mobile experience.