Introducing InnoFlow and InnoRouter: testable state management and typed navigation for SwiftUI

Hi everyone,

I’d like to share two Swift packages for apps where asynchronous feature work and navigation need clear ownership. A save operation may fail, be retried, or finish after cancellation. Its business result and the decision to open another screen are separate outcomes, and both should be testable.

Two independently adoptable roles

InnoFlow provides reducer-based state management, explicit dependencies, cancellable effects, typed outputs, and an exhaustive TestStore. A feature owns business state and emits events for its application boundary. It can work with an existing coordinator or native SwiftUI navigation, and includes its own SwiftUI presentation helpers.

InnoRouter provides typed route declarations through @Router, a RouterStore that applies navigation policies, and hosts that render native SwiftUI containers. It can be adopted without InnoFlow. Using both gives an explicit place to translate a feature’s result into a navigation request.

A memo save and its application boundary

I built a small consumer using the published tags. A scripted save adapter fails on its first attempt and succeeds on retry. Failure stays in feature state. Success records the memo ID and emits a typed output; the application boundary requests the detail screen.

These are excerpts from the tested consumer, not a complete standalone example. The full consumer has not been published, so there is no public consumer link yet. State, action, dependency, task ownership, and route declarations are omitted here.

// From the InnoFlow feature declaration:
public enum Output: Equatable, Sendable {
    case saved(id: UUID)
}

// From its reducer's successful completion branch:
case .saved(let result):
    guard state.isSaving, result.attempt == state.attempt else { return .none }
    state.isSaving = false
    state.savedID = result.id
    return Self.output(.saved(id: result.id))

The app captures outputs before dispatch enqueue, including synchronous output. This feature emits at most one saved event per dispatch, so a buffer of one is sufficient. The store-wide outputs() stream is live and does not replay earlier events.

// From the app-owned save task:
let captured = feature.send(.save, capturingOutputs: .bufferingOldest(1))
for await output in captured.outputs {
    guard !Task.isCancelled, generation == ticket else { continue }
    switch output {
    case .saved(let id): await openSavedMemo(id: id)
    }
}
await captured.finish()

openSavedMemo checks the saved ID and reserves it before awaiting:

let outcome = await router.perform(.pushIfNeeded(.detail(id: id)))
navigationOutcome = outcome
switch outcome {
case .applied, .unchanged: handledIDs.insert(id)
case .rejected, .deferred: break
}

In-flight and handled IDs suppress duplicate navigation during this model’s lifetime. Policy rejection preserves the successful save; navigation can be retried without saving again. Cancellation does not roll back an external save that already completed. Deferral requires additional application handling.

Verification and adoption cost

The exact-tag consumer passed 14 focused Swift Testing tests on Xcode 27.1, Swift 6.4, and macOS 26.7.1, with strict concurrency and warnings as errors. They cover failure, retry, typed output, late completion, caller cancellation, duplicate navigation, policy rejection, and host declarations. The adapter is scripted in memory. This integration run did not validate button-driven E2E, iOS devices, or the full library release matrix.

Adoption adds macro compilation and an explicit state/action model. Application-owned router stores also need matching host declarations. You can start with either package and add the second when that boundary helps.

Optional agent skills and availability

Both repositories include version-scoped skills for Codex and Claude Code, maintained alongside API guidance and consumer fixtures: InnoFlow skills and InnoRouter skills. Skill installation is separate from SwiftPM installation.

Both packages are MIT licensed. Published releases are InnoFlow 6.0.2 and InnoRouter 7.0.0. They require Swift 6.3+ and iOS/iPadOS/tvOS 18+, macOS 15+, watchOS 11+, or visionOS 2+.

I’d appreciate feedback on:

  • Is dispatch-scoped output capture clear when adapting an existing coordinator?
  • Where would native NavigationStack remain the simpler choice?
  • Which failure or cancellation scenario should the first public consumer demonstrate next?

InnoFlow: GitHub·README · 6.0.2 release notes

InnoRouter: GitHub·README · 7.0.0 release notes