From a66c5708523dcf6b2ab4a898ce57e710bc2ed961 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Wed, 7 Jan 2026 21:37:45 +0100 Subject: [PATCH] Add lightweight clients reference and index entry Introduces a new reference file describing the lightweight, closure-based client pattern for SwiftUI apps. Updates the components index to include a link to this new guidance, supporting easier discovery and usage of small, testable API clients. --- .../references/components-index.md | 1 + .../references/lightweight-clients.md | 93 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 swiftui-ui-patterns/references/lightweight-clients.md diff --git a/swiftui-ui-patterns/references/components-index.md b/swiftui-ui-patterns/references/components-index.md index d6c4a89..b2ac835 100644 --- a/swiftui-ui-patterns/references/components-index.md +++ b/swiftui-ui-patterns/references/components-index.md @@ -26,6 +26,7 @@ Use this file to find component-specific guidance. Each entry lists when to use - Deep links and URL routing: `references/deeplinks.md` — Use for in-app navigation from URLs. - Title menus: `references/title-menus.md` — Use for filter or context menus in the navigation title. - Loading & placeholders: `references/loading-placeholders.md` — Use for redacted skeletons, empty states, and loading UX. +- Lightweight clients: `references/lightweight-clients.md` — Use for small, closure-based API clients injected into stores. ## Planned components (create files as needed) diff --git a/swiftui-ui-patterns/references/lightweight-clients.md b/swiftui-ui-patterns/references/lightweight-clients.md new file mode 100644 index 0000000..8313d4f --- /dev/null +++ b/swiftui-ui-patterns/references/lightweight-clients.md @@ -0,0 +1,93 @@ +# Lightweight Clients (Closure-Based) + +Use this pattern to keep networking or service dependencies simple and testable without introducing a full view model or heavy DI framework. It works well for SwiftUI apps where you want a small, composable API surface that can be swapped in previews/tests. + +## Intent +- Provide a tiny "client" type made of async closures. +- Keep business logic in a store or feature layer, not the view. +- Enable easy stubbing in previews/tests. + +## Minimal shape +```swift +struct SomeClient { + var fetchItems: (_ limit: Int) async throws -> [Item] + var search: (_ query: String, _ limit: Int) async throws -> [Item] +} + +extension SomeClient { + static func live(baseURL: URL = URL(string: "https://example.com")!) -> SomeClient { + let session = URLSession.shared + return SomeClient( + fetchItems: { limit in + // build URL, call session, decode + }, + search: { query, limit in + // build URL, call session, decode + } + ) + } +} +``` + +## Usage pattern +```swift +@MainActor +@Observable final class ItemsStore { + enum LoadState { case idle, loading, loaded, failed(String) } + + var items: [Item] = [] + var state: LoadState = .idle + private let client: SomeClient + + init(client: SomeClient) { + self.client = client + } + + func load(limit: Int = 20) async { + state = .loading + do { + items = try await client.fetchItems(limit) + state = .loaded + } catch { + state = .failed(error.localizedDescription) + } + } +} +``` + +```swift +struct ContentView: View { + @Environment(ItemsStore.self) private var store + + var body: some View { + List(store.items) { item in + Text(item.title) + } + .task { await store.load() } + } +} +``` + +```swift +@main +struct MyApp: App { + @State private var store = ItemsStore(client: .live()) + + var body: some Scene { + WindowGroup { + ContentView() + .environment(store) + } + } +} +``` + +## Guidance +- Keep decoding and URL-building in the client; keep state changes in the store. +- Make the store accept the client in `init` and keep it private. +- Avoid global singletons; use `.environment` for store injection. +- If you need multiple variants (mock/stub), add `static func mock(...)`. + +## Pitfalls +- Don’t put UI state in the client; keep state in the store. +- Don’t capture `self` or view state in the client closures.