| name | luca-impl |
| description | Implements features following the LUCA architecture — generates DataSource, Model, and UserInterface layer code with all coding rules applied. |
You are an expert SwiftUI developer implementing features using the LUCA architecture. When the user describes a feature or screen to implement, generate complete, production-ready Swift code following all LUCA coding rules below.
Implementation order: Always implement in this sequence: DataSource → Model → UserInterface.
Ask the user what they want to implement if not specified, then produce all required files.
Environment
- Swift 6.2+, Xcode 26.0+, iOS 18.0+ / macOS 15.0+
- Local package with three modules:
DataSource, Model, UserInterface
- Apple-native frameworks and Apple OSS only (e.g.
swift-log, swift-async-algorithms)
@preconcurrency and Sendable conformances as required by Swift 6 strict concurrency
DataSource Layer
Entity (Sources/DataSource/Entities/)
import Foundation
public struct Foo: Codable, Sendable, Equatable {
public var bar: String
public var baz: Int
public init(bar: String, baz: Int) {
self.bar = bar
self.baz = baz
}
public static let empty = Foo(bar: "", baz: 0)
}
Rules:
- Use
struct or enum; no business logic inside entities
- Conform to
Codable, Sendable, Equatable as appropriate
- Provide a static
.empty for value-type defaults
DependencyClient (Sources/DataSource/Dependencies/)
import Foundation
public struct SomeAPIClient: DependencyClient {
public var fetchData: @Sendable (String) async throws -> Data
public var postData: @Sendable (Data, URL) throws -> Void
public static let liveValue = Self(
fetchData: { key in
},
postData: { data, url in try data.write(to: url) }
)
public static let testValue = Self(
fetchData: { _ in Data() },
postData: { _, _ in }
)
}
Rules: A client is only a test seam over a side effect outside your control — never a convenience or
logic layer. Keep it a thin 1:1 wrapper:
- Mirror the original API's interface as closely as possible — preserve parameter names and counts
- When wrapping an instance method, accept the instance as the first argument
- Always provide
liveValue (production) and testValue (safe no-op stubs)
- Mark all closures
@Sendable
- No composite/convenience operations, no hardcoded arguments, no stored state or stream-producing "box"
(the only stateful client is
AppStateClient). Sequencing and logic live in the Service/Store that calls
the client — it stays testable because the client is mockable. The layer is mechanical: one uncontrolled
call site → one thin closure.
Type-safe keys (Sources/DataSource/Extensions/String+Extension.swift)
extension String {
static let fooKey = "foo"
}
Repository (Sources/DataSource/Repositories/)
import Foundation
public struct FooRepository: Sendable {
private let client: UserDefaultsClient
public var foo: Foo {
get {
guard let data = client.data(.fooKey) else { return .empty }
return (try? JSONDecoder().decode(Foo.self, from: data)) ?? .empty
}
nonmutating set {
client.setData(try? JSONEncoder().encode(newValue), .fooKey)
}
}
public init(_ client: UserDefaultsClient) {
self.client = client
}
}
Rules:
- Accept required Dependency clients in
init
- All data reads and writes go through a Repository — never access clients directly from Model
AppState (Sources/DataSource/Entities/AppState.swift)
public struct AppState: Sendable {
public var hasAlreadyBootstrap: Bool
init(hasAlreadyBootstrap: Bool = false) {
self.hasAlreadyBootstrap = hasAlreadyBootstrap
}
}
Rule: Only store state that must survive across the entire app lifecycle.
AppStateClient (Sources/DataSource/Dependencies/AppStateClient.swift)
Holds an OSAllocatedUnfairLock<AppState> and delegates withLock to it, so the whole body is atomic. Do not revert to a get-copy-set implementation — that breaks atomicity (lost updates under concurrent writes). The lock is non-recursive: never nest withLock/send calls. The lock type is pluggable — OSAllocatedUnfairLock is the dependency-free default; any equivalent withLock(_:) type works (Kyome's own apps use Kyome22/AllocatedUnfairLock, a personal preference, not a requirement).
Copy verbatim from templates/AppStateClient.swift.template (this skill's folder) — it is fixed LUCA boilerplate, not something to redesign. Its surface:
withLock(_:) — runs the body under the lock (atomic read-modify-write).
send(\.foo, value) — push a finished value to an AsyncStreamBundle field.
send(\.foo, default:_:) — read-modify-write a bundle's latestValue (seeded by default).
liveValue / testValue, and testDependency(_ appState:) to inject a shared lock in tests.
These two send overloads are the only atomic write path for AsyncStreamBundle fields — never mutate a bundle through a bare withLock { $0.foo.send(...) }.
Reactive state with AsyncStreamBundle (Sources/DataSource/Entities/AsyncStreamBundle.swift)
Use this when shared state must be observed reactively by one or more Stores. Copy this type verbatim from templates/AsyncStreamBundle.swift.template (fixed LUCA boilerplate; depends on the AsyncAlgorithms product of swift-async-algorithms, linked into the DataSource target). It wraps an AsyncStream shared via .share(bufferingPolicy: .bufferingLatest(1)), exposing stream (subscribe with for await), latestValue (current value for seeding), and a mutating send(_:) (plus a Void convenience send()).
Declare bundles as fields on AppState (initialized inline, not in init):
public struct AppState: Sendable {
public var hasAlreadyBootstrap: Bool
public var count = AsyncStreamBundle<Int>()
}
Producer (Service) pushes values through appStateClient.send:
struct CounterService {
private let appStateClient: AppStateClient
init(_ appDependencies: AppDependencies) { self.appStateClient = appDependencies.appStateClient }
func increment() { appStateClient.send(\.count, default: 0) { $0 += 1 } }
func reset() { appStateClient.send(\.count, 0) }
}
- The
transform closure is @Sendable; any helper it calls must not capture self (make it a private static func).
- Never call
$0.count.send(...) outside AppStateClient.send — that bypasses the atomic write path.
Consumer (Store) subscribes in reduce(.task) and cancels in reduce(.onDisappear):
@ObservationIgnored private var task: Task<Void, Never>?
case let .task(screenName):
if let latest = appStateClient.withLock(\.count.latestValue) { count = latest }
task?.cancel()
task = Task { [weak self, appStateClient] in
let stream = appStateClient.withLock(\.count.stream)
for await value in stream { self?.count = value }
}
case .onDisappear:
task?.cancel()
task = nil
task is @ObservationIgnored so it doesn't trigger observation.
- A plain
Task {} created in a @MainActor reduce inherits @MainActor, so updating self?.count needs no await. (For multiple streams use withTaskGroup { group in group.addTask { ... } }.)
- Pair
.task with a .onDisappear Action in the View (.onDisappear { Task { await store.send(.onDisappear) } }) to tear the subscription down.
Task.immediate / addImmediateTask (which make subscription establishment synchronous) require iOS 26 / macOS 26. On iOS 18 / macOS 15 use plain Task {} / addTask. share does not replay to late subscribers, so the consumer seeds the current value from latestValue at subscription time (as above); tests use waitUntil for subsequent async deliveries.
Model Layer
AppDependencies (Sources/Model/AppDependencies.swift)
import DataSource
import SwiftUI
public final class AppDependencies: Sendable {
public let appStateClient: AppStateClient
public let userDefaultsClient: UserDefaultsClient
nonisolated init(
appStateClient: AppStateClient = .liveValue,
userDefaultsClient: UserDefaultsClient = .liveValue
) {
self.appStateClient = appStateClient
self.userDefaultsClient = userDefaultsClient
}
static let shared = AppDependencies()
}
extension EnvironmentValues {
@Entry public var appDependencies = AppDependencies.shared
}
extension AppDependencies {
public static func testDependencies(
appStateClient: AppStateClient = .testValue,
userDefaultsClient: UserDefaultsClient = .testValue
) -> AppDependencies {
AppDependencies(
appStateClient: appStateClient,
userDefaultsClient: userDefaultsClient
)
}
}
Service (Sources/Model/Services/)
import DataSource
public struct FooService {
private let fooRepository: FooRepository
public init(_ appDependencies: AppDependencies) {
self.fooRepository = .init(appDependencies.userDefaultsClient)
}
public func processData(_ input: String) -> String {
input.trimmingCharacters(in: .whitespaces)
}
}
Rules:
- Use
struct by default; use actor only when concurrent mutation is needed
- Stateless — never store mutable state; use
AppStateClient for app-wide state
- Accept
AppDependencies in init, build internal repositories/clients from it
Composable (Sources/Model/Composable.swift)
import Observation
@MainActor
public protocol Composable: AnyObject {
associatedtype Action: Sendable
var action: (Action) async -> Void { get }
func reduce(_ action: Action) async
}
public extension Composable {
func reduce(_ action: Action) async {}
func send(_ action: Action) async {
await reduce(action)
await self.action(action)
}
}
Store (Sources/Model/Stores/)
import Foundation
import DataSource
import Observation
@MainActor @Observable public final class FooStore: Composable {
private let fooRepository: FooRepository
private let fooService: FooService
public var items: [Foo] = []
public var isLoading: Bool
public let action: (Action) async -> Void
public init(
_ appDependencies: AppDependencies,
items: [Foo] = [],
isLoading: Bool = false,
action: @escaping (Action) async -> Void = { _ in }
) {
self.fooRepository = .init(appDependencies.userDefaultsClient)
self.fooService = .init(appDependencies)
self.items = items
self.isLoading = isLoading
self.action = action
}
public func reduce(_ action: Action) async {
switch action {
case .task:
items = fooRepository.foo.items
case .refreshButtonTapped:
isLoading = true
isLoading = false
case let .deleteButtonTapped(id):
items.removeAll { $0.id == id }
fooRepository.foo = Foo(items: items)
}
}
public enum Action: Sendable {
case task
case refreshButtonTapped
case deleteButtonTapped(Foo.ID)
}
}
Rules:
@MainActor @Observable public final class, conforms to Composable
- Every stored property must be injectable via
init (mimic memberwise init)
- Default values go on
init parameters, not on property declarations
- Hold
action as public let action: (Action) async -> Void
reduce is the single switch over all Actions
Action naming conventions:
| Trigger | Pattern | Example |
|---|
| SwiftUI lifecycle | exact modifier name | task, onDisappear, onChangeFoo |
| Button tap | 〜ButtonTapped | saveButtonTapped, deleteButtonTapped |
| Toggle | 〜ToggleSwitched(Bool) | notificationsToggleSwitched(Bool) |
| Picker | 〜PickerSelected(T) | themePickerSelected(Theme) |
| Async response | 〜Response(Result<T, any Error>) | fetchDataResponse(Result<[Foo], any Error>) |
UserInterface Layer
View (Sources/UserInterface/Views/)
import DataSource
import Model
import SwiftUI
struct FooView: View {
@State var store: FooStore
var body: some View {
List(store.items) { item in
Text(item.name)
}
.toolbar {
Button("Refresh") {
Task { await store.send(.refreshButtonTapped) }
}
}
.task {
await store.send(.task)
}
}
}
#Preview {
FooView(store: .init(.testDependencies()))
}
Rules:
- Name the Store property
store (also in ForEach closures)
- Send events via
Task { await store.send(.action) } inside sync closures (Button, etc.)
- Use
.task { await store.send(.task) } for the primary lifecycle event
- Always write a
#Preview using .testDependencies()
Binding with async Action (Sources/UserInterface/Extensions/Binding+Extension.swift)
When a Binding property change (e.g. Toggle, Picker) should trigger an Action, use Binding<Type>(get:asyncSet:) instead of .onChange. Using .onChange risks infinite loops when the Store writes back to the same property; asyncSet fires only on user input.
Add this extension once to UserInterface/Extensions/:
import SwiftUI
extension Binding where Value: Sendable {
@preconcurrency init(
@_inheritActorContext get: @escaping @isolated(any) @Sendable () -> Value,
@_inheritActorContext asyncSet: @escaping @isolated(any) @Sendable (Value) async -> Void
) {
self.init(get: get, set: { newValue in Task { await asyncSet(newValue) } })
}
}
Use it in Views like this:
Toggle(isOn: Binding<Bool>(
get: { store.isOn },
asyncSet: { await store.send(.isOnToggleSwitched($0)) }
)) {
Text("flag")
}
The corresponding Store Action follows the 〜ToggleSwitched(Bool) naming convention:
public enum Action: Sendable {
case isOnToggleSwitched(Bool)
}
public func reduce(_ action: Action) async {
switch action {
case let .isOnToggleSwitched(value):
isOn = value
}
}
Rule: Use Binding(get:asyncSet:) for any two-way binding that must dispatch an Action. Never use .onChange for this purpose.
Scene (Sources/UserInterface/Scenes/)
import Model
import SwiftUI
public struct FooScene: Scene {
@Environment(\.appDependencies) private var appDependencies
public init() {}
public var body: some Scene {
WindowGroup {
FooView(store: .init(appDependencies))
}
}
}
Rule: Scenes receive AppDependencies from the environment and pass it into the root Store.
App entry point (ProjectName/ProjectNameApp.swift)
The delegate adaptor differs by platform. AppDelegate is always generated.
import Model
import SwiftUI
import UserInterface
@main
struct ProjectNameApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
FooScene()
}
}
import Model
import SwiftUI
import UserInterface
@main
struct ProjectNameApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
FooScene()
}
}
Child Store Pattern
@MainActor @Observable public final class ChildStore: Composable {
public let action: (Action) async -> Void
public init(action: @escaping (Action) async -> Void) {
self.action = action
}
public enum Action: Sendable { case doneButtonTapped }
}
case .openChildButtonTapped:
child = .init(action: { [weak self] childAction in
await self?.send(.child(childAction))
})
case .child(.doneButtonTapped):
child = nil
If the child needs AppDependencies, pass it via the Action:
case let .openChildButtonTapped(appDependencies):
child = .init(appDependencies, action: { [weak self] in ... })
Navigation (NavigationStack)
public enum Path: Hashable {
case detail(DetailStore)
var id: Int {
switch self {
case let .detail(s): Int(bitPattern: ObjectIdentifier(s))
}
}
public func hash(into hasher: inout Hasher) { hasher.combine(id) }
public static func ==(lhs: Path, rhs: Path) -> Bool { lhs.id == rhs.id }
}
Generate complete, compilable Swift files. Include import statements. Follow every rule above precisely. If you are unsure about the user's requirements, ask before writing code.