Atoms 是一个强大且灵活的原子状态管理库,专为 Swift 设计,用于创建紧凑、独立的全局状态组件,并具有无缝的适应性和组合性。
// Create a text atom
let textAtom = Atom("")
// Create a derived atom that depends on textAtom.
// Atoms automatically update their state when any of their dependencies change.
let extractedNumbersAtom = DerivedAtom {
@UseAtomValue(textAtom) var text
return text.filter {
$0.isNumber
}
}
struct ContentView: View {
// Provide write access to the textAtom
@UseAtom(textAtom) var text
// Provide read-only access to the extractedNumbersAtom
@UseAtomValue(extractedNumbersAtom) var numbers
var body: some View {
VStack {
TextField("", text: $text)
Text("Extracted numbers: \(numbers)")
}
}
}
SwiftUI 提供了强大的内置状态处理支持,但其面向对象的方法可能会使代码拆分具有挑战性。 这就是 Atoms 可以发挥作用的地方。
Atoms 提供了更细粒度的状态管理,使您可以专注于所需的内容,而不必担心将内容放在何处。 通过避免使用具有多个已发布属性的大型可观察对象,Atoms 可帮助您避免由于渲染而导致的性能瓶颈,同时在应用程序的架构中维护单一的事实来源。
Atoms 附带 9 种不同的原子类型,应该可以满足您的大部分需求,例如处理异步。
let searchTextAtom = Atom("")
let apiAtom = Atom(...)
// Define a dogsAtom for fetching dogs based on the search text
let dogsAtom = AsyncAtom<[Dog]> {
@UseAtomValue(searchTextAtom, debounce: 0.3) var searchText
@UseAtomValue(apiAtom) var api
return try await api.searchDogs(searchText)
}
struct SearchDogsView: View {
@UseAtom(searchTextAtom) var searchText
@UseAtomValue(dogsAtom) var dogsState
var body: some View {
NavigationStack {
List {
switch dogsState {
case .loading:
ProgressView()
case .success(let dogs):
ForEach(dogs) {
Text($0.name)
}
case .failure(let error):
Text(error.localizedDescription)
Button("Try again") {
dogsAtom.reload()
}
}
}
.searchable(text: $searchText)
}
}
}
所有接受闭包作为其初始参数的原子,都会在其依赖项更改时自动更新。
T
的值的状态。T
的值或引发错误的异步操作,状态表示为 AsyncState<T>
。T
的值或抛出错误的异步序列的状态,状态表示为 AsyncState<T>
。T
的值的自定义 getter 和 setter。T
且符合 ObservableObject
的值的可读状态。Publisher
的可读状态,状态表示为 AsyncState<T>
。T
的 Published
属性的可读状态。T
的值,并在更新存储的值之前执行自定义逻辑。AnyPublisher<T, Never>
,它发出原子的当前值以及任何后续更新。Atoms 支持通过依赖注入来测试和覆盖值。
struct SearchDogsView_Previews: PreviewProvider {
static var previews: some View {
SearchDogsView()
.inject(dogsAtom) {
return .success([
.init(name: "Pluto"),
.init(name: "Lassie")
])
}
}
}
对于测试,可以使用 TestStore
。
@MainActor
func testDogsSuccess() async throws {
let mock: [Dog] = [.init(name: "Pluto"), .init(name: "Lassie")]
try await TestStore { store in
store.inject(apiAtom) {
.init(searchDogs: { _ in
return mock
})
}
@CaptureAtomValue(dogsAtom) var dogsState: AsyncState<[Dog]>
@CaptureAtom(searchTextAtom) var searchText: String
searchText = "Foo"
try await expectEqual(dogsState, .success(mock))
}
}
默认情况下,原子值仅在被主动使用时才存储在内存中。 但是,如果需要在创建原子时传递 keepAlive: true
,仍然可以保持某些值处于活动状态。
Atoms 提供了内置的调试支持,可帮助您跟踪状态更改。 在 View
上使用 enableAtomLogging
方法。
Text("Hello, World!")
.enableAtomLogging()
或直接通过 AtomStore
。
AtomStore.shared.enableAtomLogging(debugScope: .include([counterAtom]))
https://github.com/bangerang/swift-atoms.git
。Atoms 捆绑了 AsyncExpectations,这使得编写异步测试变得容易。 使用 TestStore
可确保您的测试在隔离的上下文中运行。
@MainActor
func testFilterCompletedTodos() async throws {
try await TestStore { store in
let firstMock = Todo(name: "Todo1")
let secondMock = Todo(name: "Todo2", completed: true)
let mock: [Todo] = [firstMock, secondMock]
store.inject(todosAtom) {
return mock
}
@CaptureAtom(filterTodosOptionAtom) var filterTodosOption: FilterOption
@CaptureAtomValue(filteredTodosAtom) var filteredTodos: [Todo]
filterTodosOption = .completed
try await expectEqual(filteredTodos, [secondMock])
}
}
许多问题可以通过查看文档来回答。 另外,欢迎在讨论区提问。
如果全局命名空间不是您想要的,您可以随时创建静态 let 属性来进行范围限定。
enum MyAtoms {
static let atom = Atom("")
static let derived = DerivedAtom {
@UseAtomValue(atom) var someValue
return someValue.filter {
$0.isNumber
}
}
}
除了 SwiftUI 之外,Atoms 还可以与 UIKit 一起使用。 您可以使用 @CaptureAtomPublisher
来订阅任何原子值的更改。
class ViewController: UIViewController {
@CaptureAtomPublisher(searchTextAtom) var searchTextPublisher
private let label = UILabel()
private var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(label)
cancellable = searchTextPublisher
.sink { [weak self] text in
self?.label.text = text
}
}
}
在 Xcode < 14.3 中,内联使用属性包装器而不使用后续关键字会导致编译器错误。 解决方法是添加分号或明确声明类型。
let someAtom = DerivedAtom {
@UseAtomValue(someOtherAtom) var value: String
print(value)
return "Hello " + value
}
在大多数情况下,原子将在全局范围内定义。 但是可以动态创建新原子,或者使用标准的 SwiftUI 约定(例如绑定)来避免这种情况。
使用绑定。
let personsAtom = Atom<[Person]>([Person(name: "John", age: 26)])
struct ParentView: View {
@UseAtom(personsAtom) var persons
var body: some View {
List($persons) { $person in
PersonView(person: $person)
}
}
}
struct PersonView: View {
@Binding var person: Person
var body: some View {
TextField("Name", text: $person.name)
}
}
或创建一个新的原子以获得更多控制权。
let personsAtom = Atom<[Person]>([Person(name: "John", age: 26)])
struct ParentView: View {
@UseAtom(personsAtom) var persons
var body: some View {
List(persons) { person in
PersonView(personAtom: Atom(person).onUpdate(skip: 1, { newValue in
guard let index = persons.firstIndex(where: { $0.id == newValue.id }) else {
return
}
persons[index] = newValue
}))
}
}
}
struct PersonView: View {
@UseAtom var person: Person
init(personAtom: Atom<Person>) {
self._person = UseAtom(personAtom)
}
var body: some View {
TextField("Name", text: $person.name)
}
}