| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
AsyncViewModel is a Swift Package that reimagines the original Combiner pattern using pure Swift Concurrency.
It gives you the familiar action → mutation → state pipeline without touching Combine publishers or third-party dependencies, allowing you to model view logic entirely with async/await.
flowchart TD
View --> Action
Action --> VM["AsyncViewModel"]
VM --> State
State --> View
Add the package to your Package.swift:
.package(url: "https://github.com/velos/AsyncViewModel", branch: "main")and declare the dependency where you need it:
.target(
name: "YourFeature",
dependencies: [
.product(name: "AsyncViewModel", package: "AsyncViewModel")
]
)import AsyncViewModel
import Observation
@Observable
final class CounterViewModel: AsyncViewModel {
struct State: Sendable {
var count: Int = 0
var isLoading = false
}
enum Action: Sendable {
case increment
case loadPreset
}
enum Mutation: Sendable {
case setCount(Int)
case setLoading(Bool)
}
let initialState = State()
func mutate(action: Action) async -> MutationStream {
switch action {
case .increment:
return .just(.setCount(currentState.count + 1))
case .loadPreset:
return MutationStream { continuation in
continuation.yield(.setLoading(true))
Task {
try? await Task.sleep(nanoseconds: 250_000_000)
continuation.yield(.setCount(42))
continuation.yield(.setLoading(false))
continuation.finish()
}
}
}
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let .setCount(value):
state.count = value
case let .setLoading(flag):
state.isLoading = flag
}
return state
}
}Consume the stream anywhere you like:
let viewModel = CounterViewModel()
Task {
for await state in viewModel.states() {
print("Count:", state.count)
}
}
viewModel.send(.increment)
viewModel.send(.loadPreset)Open the Documentation.docc catalog in Xcode to read the built-in guides:
Under the hood the default implementation coordinates three pieces:
This architecture matches the spirit of ReactorKit/Combiner while keeping the surface area focused and dependency-free.
You can throttle actions by overriding transform(action:) and reshaping the incoming stream before it reaches mutate(action:). The example below shows how to use swift-async-algorithms to slow down search requests to once every 300 ms.
Add the dependency to your Package.swift:
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
.target(
name: "YourFeature",
dependencies: [
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "AsyncViewModel", package: "AsyncViewModel")
]
)Then throttle the action stream:
import AsyncAlgorithms
import AsyncViewModel
import Observation
@Observable
final class SearchViewModel: AsyncViewModel {
struct State: Sendable {
var query: String = ""
var results: [Result] = []
}
enum Action: Sendable {
case queryChanged(String)
case resultsLoaded([Result])
}
let initialState = State()
func transform(action: ActionStream) -> ActionStream {
action.throttle(for: .milliseconds(300), clock: .continuous)
}
// mutate(action:) would start the search request and emit resultsLoaded,
// reduce(state:mutation:) would update the array.
}If your action enum contains additional cases, forward the non-query actions directly to the downstream continuation and only apply AsyncAlgorithms to the text-change stream.
This repository uses the Swift 6 Testing package API. Run the suite with:
swift testThe test suite includes coverage for:
Use the same Testing module in your own targets to create lightweight, async-aware specifications for view models built on this package.
AsyncViewModel is available under the MIT license. See LICENSE for details.
| Back | FazBrowse Home | New Git URL |