swift-composable-loadable

CI/CD codecov

一个用于可加载功能的 Swift Composable Architecture 组件。

基础知识

如果在您的应用程序中使用 The Composable Architecture (TCA),这个小库将允许您在您的功能中集成状态的异步加载。 假设应用程序有一个功能必须加载一些数据来显示给用户。 当我们使用 TCA 时,我们会将这些数据建模为功能的State,例如

@Reducer
struct WelcomeFeature {
  struct State {
    let message: String // Load from the server
  }
  // ...
}

假设在上面的 WelcomeFeature 中,状态的 message 属性将从我们的服务器加载,以便在应用程序启动时显示不同的消息。 在我们应用程序的功能中,我们可以使用 @LoadableState 来实现这一点。 首先,我们可以让状态遵循 Loadable 协议,

extension WelcomeFeature.State: Loadable {
  typealias Request = EmptyLoadRequest
}

然后在 AppFeature 中,我们可以组合 WelcomeFeature

@Reducer
struct AppFeature {
  struct State {
    @LoadableStateOf<WelcomeFeature> var welcome
  }
  enum Action {
    case welcome(LoadingActionOf<WelcomeFeature>)
  }
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      // main app feature logic
    }
    .loadable(\.$welcome, action: \.welcome) {
      WelcomeFeature()
    } load: { state in
      WelcomeFeature.State(message: try await fetchWelcomeMessageFromServer())
    }
  }
}

自定义请求类型

在上面的例子中,加载函数不需要任何输入。 本质上,它具有 () async throws -> Value 的形式。 然而,在许多情况下,需要提供一个输入,我们称之为请求,即 (Request) async throws -> Value。 为了做到这一点,在遵循 Loadable 协议时,我们可以指定 Request 类型。

extension WelcomeFeature.State: Loadable {
  typealias Request = WelcomeMessageRequest
}

在这种情况下,.loadable() reducer 修饰符将被请求丰富,如下所示

struct AppFeature {
  // ...
  var body: some ReducerOf<Self> {
    // ...
    .loadable(\.$welcome, action: \.welcome) {
      WelcomeFeature()
    } load: { request, state in
      WelcomeFeature.State(
        message: try await fetchWelcomeMessage(with: request)
      )
    }
  }
}

SwiftUI 视图集成

为了触发加载,所需要做的就是调用 .load() 动作。 然而,通常在视图中立即加载内容,对于这种情况,提供了一个 SwiftUI View,这使得在视图出现时加载功能变得容易。

struct AppView: View {
  let store: StoreOf<AppFeature>

  var body: some View {
    LoadingView(
      loadOnAppear: store.scope(state: \.$welcome, action: \.welcome)
    ) { store in
      Text(store.message) // the welcome message
    } onError: { error, request in
      Text("Unable to display welcome message, error: \(error.localizedDescription")
    } onActive: { request in
      ProgressView()
    }
  }
}

不同的请求

在某些情况下,将 Request 类型耦合到加载的 State 并不理想。 例如,您可能需要从不同的请求驱动相同的“结果列表”功能。 为此,可以直接在 @LoadableState 上指定 Request 类型,例如

@Reducer
struct AppFeature {
  struct State {
    @LoadableStateWith<String, WelcomeFeature> var welcome
  }
  enum Action {
    case welcome(LoadingActionWith<String, WelcomeFeature>)
  }
  // ... etc
}

在上面的例子中,不需要让 WelcomeFeature.State 遵循 Loadable 协议,而是我们可以在父功能中指定 Request 类型,在本例中为 String

Request 不是 EmptyLoadRequest 时,加载视图将需要与上面不同的初始化程序。 在这种情况下,您需要提供原始请求,例如

struct AppView: View {
  let store: StoreOf<AppFeature>

  var body: some View {
    LoadingView(
      store.scope(state: \.$welcome, action: \.welcome)
    ) { store in
      Text(store.message) // the welcome message
    } onError: { error, request in
      Text("Unable to display welcome message, error: \(error.localizedDescription")
    } onActive: { request in
      ProgressView()
    } onAppear: {
      store.send(.welcome(.load("Welcome Message Request")))
    }
  }
}