一个非常朴素的 Redux 实现,使用 Combine BindableObject 作为示例
在本小指南中,我将向您展示两种从状态中访问属性的方法,一种非常朴素,通过直接访问 store.state
全局变量或注入的 @EnvironmentObject
,另一种是如果您想使用 ConnectedView
。
首先,您必须创建一个结构体来包含您的应用程序状态,并且它需要遵循 FluxState
协议。您可以添加任何您想要的子状态。
import SwiftUIFlux
struct AppState: FluxState {
var moviesState: MoviesState
}
struct MoviesState: FluxState, Codable {
var movies: [Int: Movie] = [:]
}
struct Movie: Codable, Identifiable {
let id: Int
let original_title: String
let title: String
}
您需要的第二个部分是您的应用程序主 reducer,以及您需要的任何子状态 reducer。
import SwiftUIFlux
func appStateReducer(state: AppState, action: Action) -> AppState {
var state = state
state.moviesState = moviesStateReducer(state: state.moviesState, action: action)
return state
}
func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
var state = state
switch action {
case let action as MoviesActions.SetMovie:
state.movies[action.id] = action.movie
default:
break
}
return state
}
最后,您必须添加您的 Store
,它将包含您当前的应用程序状态 AppState
作为全局常量。
let store = Store<AppState>(reducer: appStateReducer,
middleware: nil,
state: AppState())
您需要使用您的初始应用程序状态和您的主 reducer 函数来实例化它。
现在是将其注入到您的 SwiftUI 应用程序中的部分。最常见的方法是在您的 SceneDelegate
中,当您的视图层级结构创建时进行。您应该使用提供的 StoreProvider
来包裹您的整个应用程序根视图层级结构。它会自动将 store 作为 @EnvironmentObject
注入到您的所有视图中。
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let controller = UIHostingController(rootView:
StoreProvider(store: store) {
HomeView()
})
window.rootViewController = controller
self.window = window
window.makeKeyAndVisible()
}
}
}
从那里,有两种方法可以访问您的状态属性。
在任何您想要访问应用程序状态的视图中,您可以使用 @EnvironmentObject
struct MovieDetail : View {
@EnvironmentObject var store: Store<AppState>
let movieId: Int
var movie: Movie {
return store.state.moviesState.movies[movieId]
}
//MARK: - Body
var body: some View {
ZStack(alignment: .bottom) {
List {
MovieBackdrop(movieId: movie.id)
// ...
}
}
}
}
这是一种朴素、粗暴、不太符合 Redux 规范的方式,但它确实有效。
请注意,任何您显式添加 @EnvironmentObject var store: Store<AppState>
的视图,都会在状态更新时在需要的地方重新绘制。差异由 SwiftUI 在视图级别完成。
而且它足够高效,以至于这个库不必提供自定义的订阅者或差异化机制。这是它与 UIKit 实现相比的亮点。
您也可以使用 ConnectedView
,这是新的首选方法,因为它感觉更像 Redux。但最终结果是完全相同的。您只是拥有更好的关注点分离,没有对 store.state
的随意调用,并且具有适当的本地属性。
struct MovieDetail : ConnectedView {
struct Props {
let movie: Movie
}
let movieId: Int
func map(state: AppState, dispatch: @escaping DispatchFunction) -> Props {
return Props(movie: state.moviesState.movies[movieId]!)
}
func body(props: Props) -> some View {
ZStack(alignment: .bottom) {
List {
MovieBackdrop(movieId: props.movie)
// ...
}
}
}
}
您必须实现一个 map 函数,该函数将状态中的属性转换为本地视图属性。还有一个新的 body 方法,它将在渲染时为您提供计算后的属性。
在某些时候,您需要对您的状态进行更改,为此您需要创建和分发 Action
AsyncAction
作为此库的一部分提供,是进行网络查询的正确位置,当您分发它时,它将由内部 middleware
执行。
然后,您可以在获得结果或错误时链接任何 action。
struct MoviesActions {
struct FetchDetail: AsyncAction {
let movie: Int
func execute(state: FluxState?, dispatch: @escaping DispatchFunction) {
APIService.shared.GET(endpoint: .movieDetail(movie: movie))
{
(result: Result<Movie, APIService.APIError>) in
switch result {
case let .success(response):
dispatch(SetDetail(movie: self.movie, movie: response))
case .failure(_):
break
}
}
}
}
struct SetDetail: Action {
let movie: Int
let movie: Movie
}
}
最后,您可以分发它们,如果您查看本自述文件开头的 reducer 代码,您将看到 action 是如何被 reduce 的。Reducer 是唯一允许您修改状态的函数。
由于 AppState
中的所有内容都是 Swift struct
,您实际上返回了状态的新副本,这与 Redux 架构一致。
struct MovieDetail : View {
@EnvironmentObject var store: Store<AppState>
let movieId: Int
var movie: Movie {
return store.state.moviesState.movies[movieId]
}
func fetchMovieDetails() {
store.dispatch(action: MoviesActions.FetchDetail(movie: movie.id))
}
//MARK: - Body
var body: some View {
ZStack(alignment: .bottom) {
List {
MovieBackdrop(movieId: movie.id)
// ...
}
}.onAppear {
self.fetchMovieDetails()
}
}
}