Swift

SwiftUIFlux

一个非常朴素的 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()
        }
    }
}