CacheStore

SwiftUI 状态管理

什么是 CacheStore

CacheStore 是一个 SwiftUI 状态管理框架,它使用字典作为状态。作用域的划分为父状态创建了单一数据源。CacheStore 使用 c,这是一个简单的组合框架。c 能够创建单向或双向转换。

CacheStore 基本思想

一个 [AnyHashable: Any] 可以用作应用程序的单一数据源。可以通过限制已知的键来进行作用域划分。作用域值的修改或父值的修改应该在整个应用程序中反映出来。

对象

Store

Store 是一个你可以发送动作并从中读取状态的对象。Store 使用 CacheStore 在幕后管理状态。所有状态更改都必须在 StoreActionHandler 中定义,其中状态根据动作进行修改。

TestStore

创建测试时,应使用 TestStore 来发送和接收动作,同时进行期望。如果任何期望为假,它将在 XCTestCase 中报告。如果在测试结束时有任何效果剩余,则会发生故障,因为所有效果必须完成并且所有结果动作都必须处理。TestStore 使用 FIFO(先进先出)队列来管理这些效果。

基本用法

Store 示例
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)
        }
    }
}

依赖项致谢

灵感来源