Atoms

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)
        }
    }
}

原子列表

所有接受闭包作为其初始参数的原子,都会在其依赖项更改时自动更新。

属性包装器

依赖注入

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]))

安装

Swift Package Manager

  1. 在 Xcode 中打开您的项目。
  2. 转到 File > Add Packages...
  3. 在搜索栏中,输入 Atoms 存储库的 URL:https://github.com/bangerang/swift-atoms.git
  4. 单击 Add Package
  5. 选择适当的包选项,然后再次单击 Add Package 进行确认。

测试

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
        }
    }
}

与 UIKit 一起使用

除了 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)
    }
}