AtomObjects is a lightweight state management library for SwiftUI. It allows building reusable shared and scoped states for SwiftUI applications with minimum boilerplate code.
The current version of the library is considered stable and production ready. There is no intention to make changes to the API other than bug fixes.
The main idea of AtomObject is to use small "decentralized" atom state primitives instead of a centralized store or data model. Atom objects easily allow pinpoint refreshes of SwiftUI views instead of trying to think out an efficient update strategy for bigger data models. Although it is not encouraged, you can implement complex state value provided by a single AtomObject if you'll wish so.
Add "AtomObjects" dependency via integrated Swift Package Manager in XCode
In the first step you need to implement an atom class conforming to the AtomObject protocol. This will be your shared object with the state value:
// Instead of implementing AtomObject protocol by yourself, you can just use GenericAtom class with the similar basic
// implementation as below:
class EditingAtom: AtomObject {
// Published property wrapper is needed allowing to trigger value updates.
// You can trigger an update manually by calling objectWillChange.send() where appropriate.
@Published var value: Bool
required init() {
value = false
}
}
The next step is registering unique key associated with the default atom value:
struct EditingAtomKey: AtomObjectKey {
static let defaultValue = false
}
At last you need to implement AtomRoot protocol or subclass/extend AtomObjects class and register your atom in the container. Atom object key is intended to be used as the identifier of an atom path inside root container:
extension AtomObjects {
var isEditing: EditingAtom {
get { return self[EditingAtomKey.self] }
set { self[EditingAtomKey.self] = newValue }
}
Implementation option using GenericAtom:
extension AtomObjects {
var isEditing: GenericAtom<Bool> {
get { return self[EditingAtomKey.self] }
set { self[EditingAtomKey.self] = newValue }
}
}
Put atom root scope outside of the state consuming view. Atoms will be resolved in the root container provided by the nearest scope. Scopes can be nested and injected in any view. That way you can reuse business logic associated with the specific atom root in the different places in your app.
@main
struct TheApp: App {
var body: some Scene {
WindowGroup {
AtomScope(root: AtomObjects()) {
HomeView()
}
}
}
}
You can also use view modifier on view to set new atom root. The result will be the same as wraping view with AtomScope:
@main
struct TheApp: App {
var body: some Scene {
WindowGroup {
HomeView()
.atomScope(root: AtomObjects())
}
}
}
After that, you can use @AtomState wrapper to get access to atom value in your SwiftUI view. All the views in the scope that use AtomState will automatically refresh when the atom is changed:
struct HomeView: View {
@AtomState(\AtomObjects.isEditing)
var isEditing
var body: some View {
Button {
isEditing.toggle()
} label: {
Text("Edit")
}
.popover(isPresented: $isEditing) {
EditorView()
}
}
}
If you have recurring logic applied to the atoms inside a specific root, you can wrap it inside the AtomRootAction object and reuse it in the app.
For example, we have a simple counter atom:
struct CounterAtomKey: AtomObjectKey {
static var defaultValue: Int = 0
}
class AtomObjects: AtomRoot {
var counter: AtomObject<Int> {
get { return self[CounterAtomKey.self] }
set { self[CounterAtomKey.self] = newValue }
}
}
What if you want to reuse configurable increment action in various views? It is possible by imlementing the action as in the code example below. The action in the example have a configurable increment value.
struct IncrementCounter: AtomRootAction {
var value: Int
init(by value: Int) {
self.value = value
}
func perform(with root: AtomObjects) async {
// Convenience wrapper allowing to access atom value via local variable
@AtomValue(root.counter) var counter;
counter += value
}
}
The action from the example above can be stored and cashed inside consuming view, and called then needed:
struct CounterView: View {
@AtomState(\AtomObjects.counter)
var counter
@AtomAction(AtomObjects.IncrementCounter(by: 1))
var increment
var body: some View {
Button {
increment()
} label: {
Text("Increment counter: \(counter)")
}
}
}
If you need to configure the action upon execution, it can be done by directly dispatching action from atom root:
struct CounterView: View {
@AtomState(\AtomObjects.counter)
var counter
@EnvironmentObject
var root: AtomObjects
var body: some View {
HStack {
Button {
root.dispatch(IncrementCounter(by: count * 2))
} label: {
Text("Increment")
}
.buttonStyle(.borderedProminent)
Text("Counter: \(counter)")
Button {
// It is possible to use action execution method notation
DecrementCounter(by: count * 2).perform(with: root)
} label: {
Text("Decrement")
}
.buttonStyle(.borderedProminent)
}
}
}
If you need access to multiple roots inside one action, it is possible to do so by making the action a property wrapper:
@propertyWrapper struct IncrementCounterAction: DynamicProperty, Equatable {
// Performance optimization: any action is always identical to another action of the same type
static func == (lhs: Self, rhs: Self) -> Bool { true }
@AtomBinding(\AtomObjects.counter)
var counter
var wrappedValue: (_ value: Int) -> Void {
return { value in
Task { await projectedValue(value) }
}
}
var projectedValue: (_ value: Int) async -> Void {
return { @MainActor value in
counter += value
}
}
}