{"id":17024600,"url":"https://github.com/dimillian/swiftuiflux","last_synced_at":"2025-04-05T05:05:30.984Z","repository":{"id":45251102,"uuid":"193885278","full_name":"Dimillian/SwiftUIFlux","owner":"Dimillian","description":"A very naive implementation of Redux using Combine BindableObject to serve as an example","archived":false,"fork":false,"pushed_at":"2021-01-06T02:12:48.000Z","size":48,"stargazers_count":654,"open_issues_count":12,"forks_count":62,"subscribers_count":12,"default_branch":"master","last_synced_at":"2024-10-15T07:26:10.752Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Dimillian.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["dimillian"]}},"created_at":"2019-06-26T10:44:08.000Z","updated_at":"2024-09-29T16:48:32.000Z","dependencies_parsed_at":"2022-07-13T12:50:27.010Z","dependency_job_id":null,"html_url":"https://github.com/Dimillian/SwiftUIFlux","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dimillian%2FSwiftUIFlux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dimillian%2FSwiftUIFlux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dimillian%2FSwiftUIFlux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dimillian%2FSwiftUIFlux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Dimillian","download_url":"https://codeload.github.com/Dimillian/SwiftUIFlux/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247289426,"owners_count":20914464,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T07:26:13.289Z","updated_at":"2025-04-05T05:05:30.963Z","avatar_url":"https://github.com/Dimillian.png","language":"Swift","funding_links":["https://github.com/sponsors/dimillian"],"categories":[],"sub_categories":[],"readme":"![Swift](https://github.com/Dimillian/SwiftUIFlux/workflows/Swift/badge.svg)\n\n# SwiftUIFlux\nA very naive implementation of Redux using Combine BindableObject to serve as an example\n\n## Usage\n\nIn this little guide, I'll show you two ways to access your proprerties from your state, one very naive, which works by using direct access to store.state global or injected `@EnvironmentObject` and the other one if you want to use `ConnectedView`.\n\nYou first have to make a struct which will contain your application state and it needs to conform to `FluxState`. You can add any substate you want.\n\n``` Swift\nimport SwiftUIFlux\n\nstruct AppState: FluxState {\n    var moviesState: MoviesState\n}\n\nstruct MoviesState: FluxState, Codable {\n    var movies: [Int: Movie] = [:]\n}\n\nstruct Movie: Codable, Identifiable {\n    let id: Int\n    \n    let original_title: String\n    let title: String\n}\n```\n\nThe second piece you'll need is your app main reducer, and any substate reducer you need. \n\n``` Swift\nimport SwiftUIFlux\n\nfunc appStateReducer(state: AppState, action: Action) -\u003e AppState {\n    var state = state\n    state.moviesState = moviesStateReducer(state: state.moviesState, action: action)\n    return state\n}\n\nfunc moviesStateReducer(state: MoviesState, action: Action) -\u003e MoviesState {\n    var state = state\n    switch action {\n    case let action as MoviesActions.SetMovie:\n        state.movies[action.id] = action.movie\n\n    default:\n        break\n    }\n\n    return state\n}\n```\nFinally, you have to add you `Store` which will contain you current application state `AppState` as a global constant.\n\n```Swift\nlet store = Store\u003cAppState\u003e(reducer: appStateReducer,\n                            middleware: nil,\n                            state: AppState())\n```\n\nYou instantiate with your initial application state and your main reducer function.\n\nAnd now the part where you inject it in your SwiftUI app.\nThe most common way to do it is in your `SceneDelegate` when your initiate your view hierarchy is created. You should use the provided `StoreProvider` to wrap you whole app root view hiearchy inside it. It'll auto magically inject the store as an `@EnvironmentObject` in all your views. \n\n``` Swift\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n\n    var window: UIWindow?\n\n\n    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {\n        if let windowScene = scene as? UIWindowScene {\n            let window = UIWindow(windowScene: windowScene)\n           \n            let controller = UIHostingController(rootView:\n                StoreProvider(store: store) {\n                    HomeView()\n            })\n            \n            window.rootViewController = controller\n            self.window = window\n            window.makeKeyAndVisible()\n        }\n        }\n}\n\n```\n\n\nFrom there, there are two ways to access your state properties. \n\nIn any view where you want to access your application state, you can do it using `@EnvironmentObject`\n\n``` Swift\nstruct MovieDetail : View {\n    @EnvironmentObject var store: Store\u003cAppState\u003e\n    \n    let movieId: Int\n    \n    var movie: Movie {\n        return store.state.moviesState.movies[movieId]\n    }\n\n    //MARK: - Body\n    var body: some View {\n        ZStack(alignment: .bottom) {\n            List {\n                MovieBackdrop(movieId: movie.id)\n                // ...\n            }\n        }\n    }\n}\n```\n\nThis is the naive, brutal, not so redux compliant way, but it works. \n\nNote that any view where you add explicilty add `@EnvironmentObject var store: Store\u003cAppState\u003e`will be redraw anywhere it's needed as your state is updated. The diff is done at the view level by SwiftUI. \n\nAnd it's efficient enough that this library don't have to provide custom subscribers or a diffing mechanism. This is where it shine compared to a UIKit implementation. \n\nYou can also use `ConnectedView,` this is the new prefered way to do it as it feels more redux like. But the end result is exactly the same. You just have a better separation of concerns, no wild call to store.state and proper local properties.\n\n``` Swift\nstruct MovieDetail : ConnectedView {  \n    struct Props {\n        let movie: Movie\n    }  \n\n    let movieId: Int\n    \n\n    func map(state: AppState, dispatch: @escaping DispatchFunction) -\u003e Props {\n        return Props(movie: state.moviesState.movies[movieId]!)\n    }\n\n    func body(props: Props) -\u003e some View {\n        ZStack(alignment: .bottom) {\n            List {\n                MovieBackdrop(movieId: props.movie)\n                // ...\n            }\n        }\n    }\n}\n```\nYou have to implement a map function which convert properties from your state to local view props. And also a new body method which will provide you with your computed props at render time.\n\nYou can look at more complexe examples from my app [here](https://github.com/Dimillian/MovieSwiftUI/blob/master/MovieSwift/MovieSwift/views/shared/contextMenu/MovieContextMenu.swift) and [there](https://github.com/Dimillian/MovieSwiftUI/blob/master/MovieSwift/MovieSwift/views/components/movieDetail/MovieDetail.swift).\n\n\n\nAt some point, you'll need to make changes to your state, for that you need to create and dispatch `Action`\n\n`AsyncAction` is available as part of this library, and is the right place to do network query, it'll be executed by an internal `middleware` when you dispatch it.\n\nYou can then chain any action when you get a result or an error.\n\n``` Swift\nstruct MoviesActions {\n    struct FetchDetail: AsyncAction {\n        let movie: Int\n        \n        func execute(state: FluxState?, dispatch: @escaping DispatchFunction) {\n            APIService.shared.GET(endpoint: .movieDetail(movie: movie))\n            {\n                (result: Result\u003cMovie, APIService.APIError\u003e) in\n                switch result {\n                case let .success(response):\n                    dispatch(SetDetail(movie: self.movie, movie: response))\n                case .failure(_):\n                    break\n                }\n            }\n        }\n    }\n\n    struct SetDetail: Action {\n        let movie: Int\n        let movie: Movie\n    }\n\n}\n```\n\nAnd then finally, you can dispatch them, if you look at the code of the reducer at the begining of this readme, you'll see how actions are reduced. The reducer is the only function where you are allowed to mutate your state.\n\nAs everything in the AppState are Swift `struct`, you actually return a new copy of your state, which is aligned with the Redux achitecture. \n\n``` Swift\nstruct MovieDetail : View {\n    @EnvironmentObject var store: Store\u003cAppState\u003e\n    \n    let movieId: Int\n    \n    var movie: Movie {\n        return store.state.moviesState.movies[movieId]\n    }\n\n    func fetchMovieDetails() {\n        store.dispatch(action: MoviesActions.FetchDetail(movie: movie.id))\n    }\n\n    //MARK: - Body\n    var body: some View {\n        ZStack(alignment: .bottom) {\n            List {\n                MovieBackdrop(movieId: movie.id)\n                // ...\n            }\n        }.onAppear {\n            self.fetchMovieDetails()\n        }\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdimillian%2Fswiftuiflux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdimillian%2Fswiftuiflux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdimillian%2Fswiftuiflux/lists"}