{"id":50746944,"url":"https://github.com/d-date/skip-tca","last_synced_at":"2026-06-10T22:01:28.982Z","repository":{"id":354304613,"uuid":"1222847127","full_name":"d-date/skip-tca","owner":"d-date","description":"Macro-free Composable-Architecture-shaped runtime that Skip Lite transpiles to Kotlin/Jetpack Compose.","archived":false,"fork":false,"pushed_at":"2026-04-28T01:46:30.000Z","size":33,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-28T03:07:24.006Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/d-date.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-27T19:09:20.000Z","updated_at":"2026-04-28T01:46:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/d-date/skip-tca","commit_stats":null,"previous_names":["d-date/skip-tca"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/d-date/skip-tca","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fskip-tca","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fskip-tca/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fskip-tca/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fskip-tca/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/d-date","download_url":"https://codeload.github.com/d-date/skip-tca/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fskip-tca/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34172196,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-10T22:01:27.793Z","updated_at":"2026-06-10T22:01:28.948Z","avatar_url":"https://github.com/d-date.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SkipTCA\n\nA macro-free, [Composable Architecture][tca]-shaped runtime that\n[**Skip Lite**][skip] transpiles to native Kotlin/Jetpack Compose.\n\n\u003e Write your iOS feature with `Reducer` / `Effect` / `Store`. Build it on iOS\n\u003e with Swift. Build it on Android with Skip Lite. Same source.\n\n## Why\n\n[The Composable Architecture][tca] is the de-facto state-management library in\nthe SwiftUI ecosystem, but its public API leans heavily on Swift macros\n(`@Reducer`, `@ObservableState`, `@CasePathable`, `@Dependency`, …) that\n[Skip Lite][skip] cannot expand. Trying to transpile a TCA-using feature\nunder `INCLUDE_SKIP=1 swift build` fails with hundreds of *\"Kotlin does not\nsupport this Swift attribute, macro, or property wrapper\"* warnings followed\nby a cascade of *\"Skip is unable to determine the owning type for member\"*\nerrors on every `case .binding`, `case .view(...)`, `send(.foo)` site.\n\n`SkipTCA` is the smallest reasonable replacement: it gives you the same\nshapes (`Reducer` / `Effect.run` / `Store.send` / `BindingAction` /\n`PresentationAction` / `StackState`) without using a single macro, so Skip\nLite happily emits a Kotlin sealed class for `Effect`, a Kotlin interface\nfor `Reducer`, and a Compose-aware `Store` class.\n\n```swift\nimport SkipTCA\n\nstruct Counter: Reducer {\n  typealias State = Int\n  enum Action: Sendable { case increment, decrement }\n\n  func reduce(into state: inout Int, action: Action) -\u003e Effect\u003cAction\u003e {\n    switch action {\n    case .increment: state += 1\n    case .decrement: state -= 1\n    }\n    return .none\n  }\n}\n```\n\nAfter `INCLUDE_SKIP=1 swift build` Skip Lite produces:\n\n```kotlin\nsealed class Action {\n  class IncrementCase: Action()\n  class DecrementCase: Action()\n}\n\nclass Counter: Reducer\u003cInt, Action\u003e {\n  override fun reduce(into: InOut\u003cInt\u003e, action: Action): Effect\u003cAction\u003e {\n    when (action) {\n      is Action.IncrementCase -\u003e { into.value += 1 }\n      is Action.DecrementCase -\u003e { into.value -= 1 }\n    }\n    return Effect.none\n  }\n}\n```\n\n## Status\n\n- 🚧 **Pre-1.0**, public API may change.\n- ✅ Compiles and runs on iOS / macOS / watchOS / tvOS / visionOS.\n- ✅ Transpiles cleanly under Skip Lite 1.8.11+.\n- ✅ Swift Testing test suite (39 tests).\n\n## Installation\n\nAdd SkipTCA to the dependencies of your `Package.swift`:\n\n```swift\ndependencies: [\n  .package(url: \"https://github.com/d-date/skip-tca.git\", from: \"0.1.0\"),\n],\ntargets: [\n  .target(\n    name: \"MyFeature\",\n    dependencies: [\n      .product(name: \"SkipTCA\", package: \"skip-tca\"),\n    ]\n  ),\n]\n```\n\n## API surface\n\n### Core\n\n| Type | Purpose |\n|------|---------|\n| `protocol Reducer` | Implement `reduce(into:action:) -\u003e Effect\u003cAction\u003e`. Macro-free. |\n| `enum Effect\u003cAction\u003e` | `.none`, `.run`, `.merge`, `.cancellable`, `.cancel`. Flat enum, transpiles to a Kotlin sealed class. |\n| `struct Send\u003cAction\u003e` | Closure-style action dispatcher passed to `Effect.run`. |\n| `final class Store\u003cState, Action\u003e` | `@Observable` reference container. `Store.send(_:)` runs the reducer; effects are scheduled on `@MainActor`. |\n\n### Cancellation\n\n```swift\nreturn Effect\u003cAction\u003e.cancellable(id: \"fetch\", cancelInFlight: true) { send in\n  let data = try await api.load()\n  send(.loaded(data))\n}\n\n// elsewhere\nreturn .cancel(id: \"fetch\")\n```\n\nCancellation IDs are `String` rather than `Hashable \u0026 Sendable` because Skip\ncannot transpile generic constraints over the `Hashable` protocol on free\nfunctions.\n\n### Bindings\n\n```swift\npublic enum Action: BindableAction {\n  public typealias State = MyFeature.State\n  case binding(BindingAction\u003cState\u003e)\n  case other\n}\n\n// iOS-only convenience\nlet setName: BindingAction\u003cState\u003e = .set(\\.name, to: \"Alice\")\n\n// Skip-friendly closure form (works on both platforms)\nlet setAge = BindingAction\u003cState\u003e { $0.age = 42 }\n```\n\n### View actions\n\n```swift\npublic enum Action: ViewAction, Sendable {\n  public enum ViewAction: Sendable { case onAppear, retryTapped }\n  case view(ViewAction)\n  case loaded\n}\n\nstore.send(view: .onAppear)  // shorthand for store.send(.view(.onAppear))\n```\n\n### Dependencies (iOS only)\n\n```swift\nstruct DataKey: DependencyKey {\n  static let liveValue: any DataClient = LiveDataClient()\n}\n\nstruct MyReducer: Reducer {\n  @Dependency(DataKey.self) var data\n  func reduce(into state: inout State, action: Action) -\u003e Effect\u003cAction\u003e {\n    return .run { send in\n      let value = try await data.fetch()\n      send(.loaded(value))\n    }\n  }\n}\n\nawait withDependencies {\n  $0[DataKey.self] = MockDataClient()\n} operation: {\n  // Test code that resolves DataKey to the mock.\n}\n```\n\n`@Dependency`, `withDependencies`, and `DependencyKey` are gated on\n`#if !SKIP`. Skip's coroutine-context model and Swift `TaskLocal` value\npropagation do not line up, and Skip cannot expand property wrappers. On\nAndroid, **inject your dependencies through the reducer's initializer**:\n\n```swift\nstruct MyReducer: Reducer {\n  let fetch: @Sendable () async throws -\u003e Data\n  // ...\n}\n```\n\n### Navigation\n\n```swift\npublic struct State: Sendable {\n  public var destination: PresentationState\u003cDetailFeature.State\u003e = .init()\n  public var path: StackState\u003cRouteState\u003e = StackState([])\n}\n\npublic enum Action: Sendable {\n  case destination(PresentationAction\u003cDetailFeature.Action\u003e)\n  case path(StackAction\u003cRouteState, RouteAction\u003e)\n}\n```\n\n`StackState\u003cElement\u003e` is array-backed (no `IdentifiedArray` dependency) so\nit transpiles trivially. `StackAction` indexes by position rather than ID.\n\n### TestStore (iOS only)\n\n```swift\n@MainActor\n@Test\nfunc myFeature() async {\n  let store = TestStore\u003cState, Action\u003e(initialState: .init(), reducer: MyReducer().reduce(into:action:))\n  store.dependencies[DataKey.self] = MockDataClient()\n  await store.send(.kickoff)\n  await store.receive(.loaded(.fixture)) { state in\n    #expect(state.value == .fixture)\n  }\n  await store.finish()\n}\n```\n\n`TestStore` is iOS only because it depends on Swift Testing's `Issue.record`\nand `SourceLocation`. Production reducers and stores ship to Android via\nSkip without it.\n\n## Migrating from `swift-composable-architecture`\n\n| TCA | SkipTCA |\n|-----|---------|\n| `@Reducer struct Feature {}` | `struct Feature: Reducer {}` |\n| `@ObservableState struct State {}` | plain `struct State: Sendable {}` (observability lives on `Store`) |\n| `var body: some ReducerOf\u003cSelf\u003e { Reduce { ... } }` | `func reduce(into:action:) -\u003e Effect\u003cAction\u003e { switch action { ... } }` |\n| `@CasePathable enum Action {}` | plain `enum Action: Sendable {}` |\n| `@Dependency(\\.foo) var foo` | iOS: `@Dependency(FooKey.self)`; Skip: `let foo: FooClient` injected via `init` |\n| `@DependencyClient struct Client {}` | hand-written `struct Client { var fetch: ... }` with explicit `init` |\n| `BindingReducer()` | `case .binding(let action): action.apply(to: \u0026state); return .none` |\n| `@ViewAction(for: Feature.self)` | `Store.send(view:)` extension |\n| `@Presents var destination: ...?` | `var destination: PresentationState\u003c...\u003e = .init()` |\n| `StackState` (identified) | `StackState` (array-backed) |\n| `TestStore` | `SkipTCA.TestStore` (iOS-only) |\n\n## Skip Lite compatibility notes\n\nWhile building SkipTCA we hit the following Skip Lite constraints. Each one\nis documented inline in the relevant source file:\n\n1. **Nested types inside a generic outer type** don't translate. `Effect\u003cAction\u003e.Operation` was rejected; flatten to a top-level enum case.\n2. **Static members on a generic type** can't reference the outer generic from a Kotlin companion object.\n3. **Constructors / functions can't introduce additional generics** beyond the owning type.\n4. **Generic constraints over a Swift protocol** don't translate. `func makeStore\u003cR: Reducer\u003e(...)` was rejected.\n5. **Constrained typealiases** don't translate. `typealias StoreOf\u003cR: Reducer\u003e` was rejected.\n6. **Property wrappers** don't translate; `@Dependency` is gated on `#if !SKIP`.\n7. **`AnyHashable`-based generic constraints** on free functions don't translate; we use plain `String` cancellation IDs.\n8. **`Hashable \u0026 Sendable` generic constraints** on free functions don't translate either.\n\nThese observations are useful upstream feedback for `skiptools/skip` and the\nTCA team.\n\n## Testing\n\n```bash\nswift test                            # run the Swift Testing suite\nINCLUDE_SKIP=1 swift build            # verify the Skip Lite transpiler is happy\n```\n\n## Roadmap\n\n- `IdentifiedArray`-backed `StackState` (when SkipFoundation supports it).\n- `forEach` reducer composition helper.\n- `BindingViewState` / `@Bindable` ergonomics.\n- Optional `swift-composable-architecture` interop layer (use TCA on iOS, SkipTCA on Android, sharing State/Action enums).\n- Investigate exposing Kotlin coroutine `CoroutineContext`-based dependency\n  scoping that mirrors `withDependencies` on Skip.\n\n## Inspiration \u0026 prior art\n\n- [Point-Free's swift-composable-architecture][tca] — the API we're shaping after.\n- [Skip][skip] — the Swift→Kotlin transpiler whose constraints shaped this design.\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n\n[tca]: https://github.com/pointfreeco/swift-composable-architecture\n[skip]: https://skip.tools\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fd-date%2Fskip-tca","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fd-date%2Fskip-tca","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fd-date%2Fskip-tca/lists"}