一键导入
luca-impl
Implements features following the LUCA architecture — generates DataSource, Model, and UserInterface layer code with all coding rules applied.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Implements features following the LUCA architecture — generates DataSource, Model, and UserInterface layer code with all coding rules applied.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Writes unit tests for LUCA Service and Store components using Swift Testing, with proper naming conventions and dependency mocking patterns.
Explains the LUCA architecture — its three-layer structure, key components, data flow, and patterns for SwiftUI app development.
Guides project scaffolding for LUCA-based Xcode projects — installing prerequisites, running the luca CLI, and understanding the generated project structure.
基于 SOC 职业分类
| 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.
DataSource, Model, UserInterfaceswift-log, swift-async-algorithms)@preconcurrency and Sendable conformances as required by Swift 6 strict concurrencySources/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:
struct or enum; no business logic inside entitiesCodable, Sendable, Equatable as appropriate.empty for value-type defaultsSources/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
// actual implementation
},
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:
liveValue (production) and testValue (safe no-op stubs)@SendableAppStateClient). 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.Sources/DataSource/Extensions/String+Extension.swift)extension String {
static let fooKey = "foo"
}
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:
initSources/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.
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(...) }.
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 } } // read-modify-write
func reset() { appStateClient.send(\.count, 0) } // finished value
}
transform closure is @Sendable; any helper it calls must not capture self (make it a private static func).$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.Task {} created in a @MainActor reduce inherits @MainActor, so updating self?.count needs no await. (For multiple streams use withTaskGroup { group in group.addTask { ... } }.).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 plainTask {}/addTask.sharedoes not replay to late subscribers, so the consumer seeds the current value fromlatestValueat subscription time (as above); tests usewaitUntilfor subsequent async deliveries.
Sources/Model/AppDependencies.swift)import DataSource
import SwiftUI
public final class AppDependencies: Sendable {
public let appStateClient: AppStateClient
public let userDefaultsClient: UserDefaultsClient
// add more clients here
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
)
}
}
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 {
// pure business logic — no state mutation
input.trimmingCharacters(in: .whitespaces)
}
}
Rules:
struct by default; use actor only when concurrent mutation is neededAppStateClient for app-wide stateAppDependencies in init, build internal repositories/clients from itSources/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) // internal handling first
await self.action(action) // then delegate to parent
}
}
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
// async work...
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 Composableinit (mimic memberwise init)init parameters, not on property declarationsaction as public let action: (Action) async -> Voidreduce is the single switch over all ActionsAction 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>) |
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:
store (also in ForEach closures)Task { await store.send(.action) } inside sync closures (Button, etc.).task { await store.send(.task) } for the primary lifecycle event#Preview using .testDependencies()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
// further processing...
}
}
Rule: Use Binding(get:asyncSet:) for any two-way binding that must dispatch an Action. Never use .onChange for this purpose.
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.
ProjectName/ProjectNameApp.swift)The delegate adaptor differs by platform. AppDelegate is always generated.
// iOS
import Model
import SwiftUI
import UserInterface
@main
struct ProjectNameApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
FooScene()
}
}
// macOS
import Model
import SwiftUI
import UserInterface
@main
struct ProjectNameApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
FooScene()
}
}
// Child
@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 }
}
// Parent
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 ... })
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.