Skip to content
Apple Development Foundation
Esc
↑↓navigate↵open⌘Jpreview
On this page

Multi-service iOS app patterns

Reusable architecture and agent-workflow patterns for device-to-many HTTP API apps (Keychain, FeatureState, phased integrations, inspection routes).

Patterns for device → many independent HTTP APIs apps (dashboards, service clients, and multi-service workflows). Keep examples neutral and reusable: no branded or environment-specific references, and no secrets.

Designed for dashboards, service clients, and independent service peers. Apply in the active application workspace, not this foundation’s archive/.

When to use

  • App talks to 2+ services, each with its own base URL and credential.
  • Prefer direct device→service connections for the first release.
  • Secrets must stay off disk-in-UserDefaults and out of fixtures/logs.
  • Agents will implement integrations and UI; teams set phase boundaries.

Phase slice

Ship capability in read-only slices before mutations:

Phase Goal Avoid
Foundation Registry, Keychain refs, transport, probe, one list surface Writes, search, delete
Enrichment Detail, related resources, remote assets, status aggregation Display-name “joins” across services
Transfer client Separate client for transfer UI Using the wrong service for resource availability
Mutations Pause/resume, monitor toggles, destructive ops Shipping controls before read state is trustworthy

Rule: resource-associated “in progress” state often belongs to the orchestrator API, not the transfer client. Full transfer lists belong to the transfer client. Document the join key early (stable id / hash) and ban display-name matching.

Architecture sketch

AppDependencies (@Observable / environment)
  ├─ ServiceRegistry (non-secret config → Preferences)
  ├─ KeychainStore (secrets by CredentialReference)
  ├─ HTTPTransport (mockable; scheme policy)
  ├─ ConnectionResolver (local/remote preference + probe)
  └─ Repository (aggregate FeatureState per feature)

ServiceClientFactory → per-kind actor clients (DTO in, domain out)

Domain boundary

  1. Config — kind, display name, endpoints (scheme/host/port/path), preference, opaque credential reference.
  2. Clients — private DTOs, auth headers/cookies, path construction including optional urlBase / base path.
  3. Domain models — Resource, CollectionItem, TransferItem, ServiceStatus, … only types UI should see.
  4. Repository — fan-out enabled services, merge by stable ids, map errors to recovery.

Do not leak raw JSON or service field names into SwiftUI views.

Secrets and credentials

Store What
Preferences / registry Non-secret config, credential reference (UUID account id)
Keychain API keys, tokens, username/password
Memory only Session credentials (re-authenticate on expiry)
Never Secrets in fixtures, launch args, screenshots, logs, sample data

Connection Test: probe with an ephemeral Keychain account so Test never overwrites the live secret on edit. Empty credential field on Save keeps the existing Keychain value.

Auth mapping: HTTP 401/403 → unauthorized; missing secret → missing credential; transport failure → unavailable. UI recovery: Retry vs Open Settings / re-enter credentials.

Never log tokens, passwords, session credentials, or endpoint URLs containing sensitive data.

Credential lifecycle

  • Rotate credentials by writing the replacement only after it has been validated; preserve the prior value until the replacement succeeds.
  • Deleting a credential or removing an account must remove its Keychain item, clear in-memory sessions, cancel in-flight work, and purge account-scoped caches and persisted endpoint configuration.
  • Never place credentials in query strings, logs, screenshots, fixtures, sample data, or handoff text. Use request headers or secure session storage instead.

Transport policy

  • Prefer HTTPS for remote endpoints.
  • Allow user-explicit plain HTTP only where a documented local-network requirement exists (document which endpoint labels allow it).
  • ATS: narrow exception (NSAllowsLocalNetworking or equivalent)—not NSAllowsArbitraryLoads.
  • Never disable certificate validation to “make it work.”
  • Normalize endpoints with URL components before storing or requesting them: trim input, reject malformed URLs, user-info credentials, fragments, and unsupported schemes, and enforce the configured scheme allowlist before and after redirects.
  • Timeouts, cancellation, retry/backoff, and a mockable HTTPTransport protocol are mandatory for tests. Document timeouts by request class; cancel work when its feature leaves scope; retry only idempotent transient failures with capped exponential backoff and jitter, never authentication or validation failures.

FeatureState (feature-level loading)

Use a small enum for every remote feature surface:

loading | content(value, refreshedAt) | empty | stale(value) | unavailable(recovery) | unauthorized | disabled
  • Prefer one status row per service (probe OK + content fail must not double-emit connected + failed).
  • Partial multi-service success: show connected peers + failed peers without blanking the whole app when any sibling works.
  • Recovery actions are explicit: retry, update credentials, configure endpoint, enable service.
  • Show stale values with their refresh time and recovery action. Define cache expiration per resource, avoid treating expired cache as fresh content, and purge account-scoped cache on credential deletion or account removal.

Remote assets

  • Prefer absolute remote URLs when the API provides them.
  • Relative asset paths need a normalized base URL and header-based authorization.
  • Never log authenticated asset URLs.
  • List rows use fixed compact frames + clip; do not let variable-aspect-ratio assets overflow adjacent cards.
  • Cache bytes when scroll causes probe-per-image cost.

Cross-service joins

Do Don’t
Join on service ids / hashes (case-insensitive when required) Match by display name
Keep private join fields off ordinary UI Surface raw hashes as primary labels
Document which service owns each status or badge Require one service’s credentials to show another service’s private state

System UI first: Liquid Glass

Liquid Glass is a navigation/control treatment, not an app-wide background style. This rule applies to every SwiftUI app; it is especially important in a multi-service app, where dense lists, status cards, and partial-failure states must remain readable.

  1. Use system chrome first. Prefer native TabView, navigation bars, toolbars, and sheets. On supported OS versions, let the system provide the current Liquid Glass appearance, including its material, shape, selection, safe-area behavior, and interaction.
  2. Keep the content plane opaque. Lists, tables, cards, forms, and feature surfaces should remain solid/opaque. Do not apply glass to content merely because the app uses Liquid Glass in its navigation chrome.
  3. Add custom glass only for a real navigation-plane gap. A floating action or control may use glassEffect only when a native container cannot express it; use GlassEffectContainer for coordinated glass elements. Never rebuild a tab bar or navigation bar with materials, overlays, or a custom safe-area bar.
  4. Preserve the platform contract. Keep controls around ~44×44 pt, use system text styles, support Dynamic Type and VoiceOver, maintain contrast and Differentiate Without Color, and respect Reduce Motion. Keep the native fallback on older supported OS versions instead of imitating a newer glass appearance.

The short version: when a future app request says “use Liquid Glass,” start by adopting system navigation chrome. Reach for custom glass only after confirming that system UI cannot express the needed navigation-plane control.

Agent handoff shape

Every multi-step integration handoff should include:

  1. Do not redo — already-fixed items (regression surface only).
  2. Severity / priority bands — P0 trust → P1 next client → P2 auth → P3 peers.
  3. File map — exact paths.
  4. API truth doc — prefer a fact-checked agent guide over marketing copy or unstable public specs for endpoints.
  5. Non-goals — no push, no secrets in fixtures, no write APIs this phase.
  6. Paste prompt — zero-context instruction for a fresh agent.
  7. Verification gate — scheme, test_sim parallel off, inspection screenshots when UI changes.

Inspection contract (deterministic UI)

For agent-driven UI verification:

  • Launch arguments such as --inspection <route> for loading / empty / unauthorized / primary tabs.
  • Optional --light-appearance, accessibility Dynamic Type flags.
  • For each touched user-visible route: screenshot → read the image → snapshot_ui hierarchy check.
  • Keep inspection routes covered by unit tests when adding critical surfaces.

Testing

Layer Practice
Transport Stub / path-routing mock; assert scheme policy and status mapping
Clients Decode synthetic fixtures only (fixture.invalid, fake ids)
Repository Multi-service partial failure and status dedupe
Secrets skim Grep fixtures for credentials, tokens, and passwords before commit

XcodeBuildMCP: session_show_defaults once per session; test_sim with parallel testing disabled for scheme stability; build_run_sim for smoke when UI changes.

New service client checklist

  1. ServiceKind case + registry form fields (correct credential shape).
  2. Actor client conforming to shared ServiceClient (probe).
  3. Private request helper: Keychain load + auth headers/cookies + base path.
  4. DTO → domain mapping.
  5. Register in factory.
  6. Fixtures + decode/auth tests.
  7. Icon asset (template or app-provided mark per application policy).
  8. No live network in CI.

Distribution and privacy note

If this pattern ships publicly, document endpoint security, local-network usage, and data handling in the application and privacy plan. Keep application-specific policy out of fixtures.

Anti-patterns

  • Global TLS disable or arbitrary ATS loads for one service
  • Storing API keys next to base URLs in UserDefaults
  • Sample records that look live after real service data exists
  • Display-name matching between services
  • Re-researching service APIs when a fact-checked agent guide exists
  • Expanding this foundation’s archive/Sources for app work
  • One monocommit of P0–P3 without intermediate green tests

Keep this page application-neutral. Application-specific severity lists and paste prompts live in the active workspace’s Documentation/.

Was this page helpful?