Manage Swift app state asynchronously.
The async state framework simplifies swift object state management. It encourages single responsibility principles and state encapsulation. It was designed to be used in a stateful Model View ViewModel architecture, standard for iOS application development with UIKit.
Async State is distributed via Swift Package Manager. Add the following to your Package.swift
or via XCode SPM manager.
...
dependencies: [
.package(url: "https://github.com/pschuette22/async-state", from: "1.0.0"),
],
.target(
...
dependencies: [
.product(name: "AsyncState", package: "async-state"),
.product(name: "AsyncStateMacros", package: "async-state"),
],
),
Async State comes with three major building blocks and helpers for each type. These building blocks are Events, Effects, and State.
Events are emitted from an object when something occurs. This could be a tap action, a system event, or anything that a programmer would like their feature to react to. These events are broadcast "vertically". Event broadcasters do not track the progress of an event nor do they care who receives them. They tell the world something happened and move on.
An object which sends events conforms to EventStreaming
.
final class ContactsRepository: EventStreaming {
enum StreamedEvent: Event {
case didAddContact(String)
}
// This helps with sending scope
private let _eventBroadcast = OpenAsyncBroadcast<State>()
var eventStream: any AsyncBroadcast<StreamedEvent> { _eventBroadcast }
}
Effects are the result of events. When an object receives an event AsyncState provides helpers for mapping these events to effects. This paradigm encourages compile safety.
final class AddressBookViewModel {
private let contactsRepository = ContactsRepository()
func setupContactEventSubscription() {
contactsRepository.receive(
from: contactsRepository
) { contactsRepositoryEvent in
switch contactsRepositoryEvent {
case .didAddContact(let contactId):
return .addContactCell(contactId)
}
}
}
}
extension AddressBookViewModel: EffectHandling, EventReceiving {
enum HandledEffect: Effect {
case addContactCell(String)
}
func handle(_ effect: HandledEffect) {
switch effect {
case .addContactCell(String):
// TODO: Update state to add a cell with the new contact
break
}
}
}
The AsyncState uses a type called ObjectState
or ViewState
as the primary mechanism for describing an object's distinct state. This is useful for modeling and managing application logic.
States should always be Value types. For example:
struct ContactsRepositoryState: ObjectState {
var contactIds: [String]
...
}
This encapsulation allows a StateStreaming
type of ContactRepository
to encapuslate the management of contact IDs and broadcast changes to the world.
final class ContactRepository: StateStreaming
typealias State = ContactsRepositoryState
// This helps with sending scope
private let _stateBroadcast = OpenAsyncBroadcast<State>()
var stateStream: any AsyncBroadcast<State> { _stateBroadcast }
Objects interested in state changes to the contact repository may observe the change stream directly without framework functions, as well.
let contactRepository = ContactRepository()
...
let observerTask = Task { [weak contactRepository] in
guard let contactRepository else { return }
var stateIterator = contactRepository.stateStream.observe()
while let newState = await stateIterator.next() {
// Handle new state
}
}
Install templates using the install script (likely {project-path}/.build/checkouts/async-state/xctemplates/install-xctemplates.sh
)
Run the script and templates will be installed locally. This may require an xcode restart.
Once installed, create a ModeledViewController using the latest template.