SwiftUI 状态管理
CacheStore
是一个 SwiftUI 状态管理框架,它使用字典作为状态。作用域的划分为父状态创建了单一数据源。CacheStore
使用 c
,这是一个简单的组合框架。c
能够创建单向或双向转换。
一个 [AnyHashable: Any]
可以用作应用程序的单一数据源。可以通过限制已知的键来进行作用域划分。作用域值的修改或父值的修改应该在整个应用程序中反映出来。
CacheStore
: 需要定义键来获取和设置值的对象。Store
: 需要定义键、动作和依赖关系的对象。(推荐)TestStore
: Store
的可测试包装器,使其易于编写 XCTestCasesStore
是一个你可以发送动作并从中读取状态的对象。Store 使用 CacheStore
在幕后管理状态。所有状态更改都必须在 StoreActionHandler
中定义,其中状态根据动作进行修改。
创建测试时,应使用 TestStore
来发送和接收动作,同时进行期望。如果任何期望为假,它将在 XCTestCase
中报告。如果在测试结束时有任何效果剩余,则会发生故障,因为所有效果必须完成并且所有结果动作都必须处理。TestStore
使用 FIFO(先进先出)队列来管理这些效果。
import CacheStore
import SwiftUI
struct Post: Codable, Hashable {
var id: Int
var userId: Int
var title: String
var body: String
}
enum StoreKey {
case url
case posts
case isLoading
}
enum Action {
case fetchPosts
case postsResponse(Result<[Post], Error>)
}
extension String: Error { }
struct Dependency {
var fetchPosts: (URL) async -> Result<[Post], Error>
}
extension Dependency {
static var mock: Dependency {
Dependency(
fetchPosts: { _ in
sleep(1)
return .success([Post(id: 1, userId: 1, title: "Mock", body: "Post")])
}
)
}
static var live: Dependency {
Dependency { url in
do {
let (data, _) = try await URLSession.shared.data(from: url)
return .success(try JSONDecoder().decode([Post].self, from: data))
} catch {
return .failure(error)
}
}
}
}
let actionHandler = StoreActionHandler<StoreKey, Action, Dependency> { cacheStore, action, dependency in
switch action {
case .fetchPosts:
struct FetchPostsID: Hashable { }
guard let url = cacheStore.get(.url, as: URL.self) else {
return ActionEffect(.postsResponse(.failure("Key `.url` was not a URL")))
}
cacheStore.set(value: true, forKey: .isLoading)
return ActionEffect(id: FetchPostsID()) {
.postsResponse(await dependency.fetchPosts(url))
}
case let .postsResponse(.success(posts)):
cacheStore.set(value: false, forKey: .isLoading)
cacheStore.set(value: posts, forKey: .posts)
case let .postsResponse(.failure(error)):
cacheStore.set(value: false, forKey: .isLoading)
}
return .none
}
struct ContentView: View {
@ObservedObject var store: Store<StoreKey, Action, Dependency> = .init(
initialValues: [
.url: URL(string: "https://jsonplaceholder.typicode.com/posts")!
],
actionHandler: actionHandler,
dependency: .live
)
.debug
private var isLoading: Bool {
store.get(.isLoading, as: Bool.self) ?? true
}
var body: some View {
if
!isLoading,
let posts = store.get(.posts, as: [Post].self)
{
List(posts, id: \.self) { post in
Text(post.title)
}
} else {
ProgressView()
.onAppear {
store.handle(action: .fetchPosts)
}
}
}
}
import CacheStore
import XCTest
@testable import CacheStoreDemo
class CacheStoreDemoTests: XCTestCase {
func testExample_success() throws {
let store = TestStore(
initialValues: [
.url: URL(string: "https://jsonplaceholder.typicode.com/posts") as Any
],
actionHandler: actionHandler,
dependency: .mock
)
store.send(.fetchPosts) { cacheStore in
cacheStore.set(value: true, forKey: .isLoading)
}
store.send(.fetchPosts) { cacheStore in
cacheStore.set(value: true, forKey: .isLoading)
}
let expectedPosts: [Post] = [Post(id: 1, userId: 1, title: "Mock", body: "Post")]
store.receive(.postsResponse(.success(expectedPosts))) { cacheStore in
cacheStore.set(value: false, forKey: .isLoading)
cacheStore.set(value: expectedPosts, forKey: .posts)
}
}
func testExample_failure() throws {
let store = TestStore(
initialValues: [
:
],
actionHandler: actionHandler,
dependency: .mock
)
store.send(.fetchPosts, expecting: { _ in })
store.receive(.postsResponse(.failure("Key `.url` was not a URL"))) { cacheStore in
cacheStore.set(value: false, forKey: .isLoading)
}
}
}