{"id":13465295,"url":"https://github.com/gre4ixin/ReduxUI","last_synced_at":"2025-03-25T16:31:35.018Z","repository":{"id":45126979,"uuid":"422362984","full_name":"gre4ixin/ReduxUI","owner":"gre4ixin","description":"💎 Redux like architecture for SwiftUI","archived":false,"fork":false,"pushed_at":"2022-01-08T22:55:01.000Z","size":6139,"stargazers_count":44,"open_issues_count":0,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-28T20:44:48.797Z","etag":null,"topics":["architecture","asyncawait","combine","concurrency","flux","flux-architecture","framework","ios","redux","redux-thunk","state-machine","swift","swiftpackagemanager","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gre4ixin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-28T21:40:09.000Z","updated_at":"2024-09-29T16:40:59.000Z","dependencies_parsed_at":"2022-08-25T23:01:19.107Z","dependency_job_id":null,"html_url":"https://github.com/gre4ixin/ReduxUI","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gre4ixin%2FReduxUI","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gre4ixin%2FReduxUI/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gre4ixin%2FReduxUI/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gre4ixin%2FReduxUI/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gre4ixin","download_url":"https://codeload.github.com/gre4ixin/ReduxUI/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":222088532,"owners_count":16928976,"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":["architecture","asyncawait","combine","concurrency","flux","flux-architecture","framework","ios","redux","redux-thunk","state-machine","swift","swiftpackagemanager","swiftui"],"created_at":"2024-07-31T15:00:26.136Z","updated_at":"2024-10-29T17:30:41.165Z","avatar_url":"https://github.com/gre4ixin.png","language":"Swift","funding_links":[],"categories":["Patterns","Architecture and State"],"sub_categories":["Vim"],"readme":"![logo](./logo.png)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg alt=\"Platform\" src=\"https://img.shields.io/badge/platform-iOS-orange.svg\"\u003e\n  \u003cimg alt=\"Swift\" src=\"https://img.shields.io/badge/Swift-5.5-orange.svg\"\u003e\n  \u003cimg alt=\"License\" src=\"https://img.shields.io/badge/LICENSE-MIT-blue.svg\"\u003e\n  \u003cimg alt=\"Platform Version\" src=\"https://img.shields.io/badge/iOS-13-green.svg\"\u003e\n\u003c/p\u003e\n\n\n# Simple Architecture like Redux\n\n## Installation\n### SPM\n```swift\ndependencies: [\n    .package(url: \"https://github.com/gre4ixin/ReduxUI.git\", .upToNextMinor(from: \"1.0.0\"))\n]\n```\n\n## Usage \n```swift\nimport ReduxUI\n\nclass SomeCoordinator: Coordinator {\n    func perform(_ route: SomeRoute) { }\n}\n\nenum SomeRoute: RouteType {\n\n}\n\nenum AppAction: AnyAction {\n    case increase\n    case decrease\n}\n\nstruct AppState: AnyState {\n    var counter: Int = 0\n}\n\nclass AppReducer: Reducer {\n    typealias Action = AppAction\n\n    func reduce(_ state: inout AppState, action: AppAction, performRoute: @escaping ((_ route: SomeRoute) -\u003e Void)) {\n        switch action {\n        case .increase:\n            state.counter += 1\n        case .decrease:\n            state.counter -= 1\n        }\n    }\n}\n\nclass ContentView: View {\n    @EnvironmentObject var store: Store\u003cAppState, AppAction, SomeRouter\u003e\n\n    var body: some View {\n        VSTack {\n            Text(store.state.counter)\n\n            Button {\n                store.dispatch(.increase)\n            } label: {\n                Text(\"increment\")\n            }\n\n            Button {\n                store.dispatch(.decrease)\n            } label: {\n                Text(\"decrement\")\n            }\n        }\n    }\n}\n\nclass AppModuleAssembly {\n    func build() -\u003e some View {\n        let reducer = AppReducer().eraseToAnyReducer()\n        let coordinator = SomeCoordinator().eraseToAnyCoordinator()\n        let store = Store(initialState: AppState(), coordinator: coordinator, reducer: reducer)\n        let view = ContentView().environmentObject(store)\n        return view\n    }\n}\n\n```\n\nThat was very simple example, in real life you have to use network request, action in app state changes and many other features. In these cases you can use `Middleware`.\n\n##### `Middlewares` calls after reducer function and return \n```swift\n AnyPublisher\u003cMiddlewareAction, Never\u003e\n```\n\n##### For example create simple project who fetch users from `https://jsonplaceholder.typicode.com/users`.\n\nCreate DTO (Decode to object) model\n```swift\nstruct UserDTO: Decodable, Equatable, Identifiable {\n    let id: Int\n    let name: String\n    let username: String\n    let phone: String\n}\n```\n`Equatable` protocol for our state, `Identifiable` for `ForEach` generate view in SwiftUI View.\n\n##### Simple network request without error checking\n```swift\nimport Foundation\nimport Combine\n\nprotocol NetworkWrapperInterface {\n    func request\u003cD: Decodable\u003e(path: URL, decode: D.Type) -\u003e AnyPublisher\u003cD, NetworkError\u003e\n}\n\nstruct NetworkError: Error {\n    let response: URLResponse?\n    let error: Error?\n}\n\nclass NetworkWrapper: NetworkWrapperInterface {\n    \n    func request\u003cD: Decodable\u003e(path: URL, decode: D.Type) -\u003e AnyPublisher\u003cD, NetworkError\u003e {\n        return Deferred {\n            Future\u003cD, NetworkError\u003e { promise in\n                let request = URLRequest(url: path)\n                URLSession.shared.dataTask(with: request) { [weak self] data, response, error in\n                    guard let _ = self else { return }\n                    if let _error = error {\n                        promise(.failure(NetworkError(response: response, error: _error)))\n                    }\n                    \n                    guard let unwrapData = data, let json = try? JSONDecoder().decode(decode, from: unwrapData) else {\n                        promise(.failure(NetworkError(response: response, error: error)))\n                        return\n                    }\n                    \n                    promise(.success(json))\n                    \n                }.resume()\n            }\n        }.eraseToAnyPublisher()\n    }\n    \n}\n```\n\n##### Make `State`, `Action` and `Reducer`\n\n```swift\nenum AppAction: AnyAction {\n    case fetch\n    case isLoading\n    case loadingEnded\n    case updateUsers([UserDTO])\n    case error(message: String)\n}\n\nstruct AppState: AnyState {\n    var users: [UserDTO] = []\n    var isLoading = false\n    var errorMessage = \"\"\n}\n\nclass AppReducer: Reducer {\n    typealias Action = AppAction\n    \n    func reduce(_ state: inout AppState, action: AppAction, performRoute: @escaping ((RouteWrapperAction) -\u003e Void)) {\n        switch action {\n        case .fetch:\n            state.isLoading = true\n            state.errorMessage = \"\"\n        case .isLoading:\n            state.isLoading = true\n        case .loadingEnded:\n            state.isLoading = false\n        case .updateUsers(let users):\n            state.users = users\n            state.isLoading = false\n            state.errorMessage = \"\"\n        case .error(let message):\n            state.errorMessage = message\n        }\n    }\n}\n```\n\n##### Middleware for make network request and return `users DTO`.\n\n```swift\nclass AppMiddleware: Middleware {\n    typealias State = AppState\n    typealias Action = AppAction\n    typealias Router = RouteWrapperAction\n    \n    let networkWrapper: NetworkWrapperInterface\n    \n    var cancelabels = CombineBag()\n    \n    init(networkWrapper: NetworkWrapperInterface) {\n        self.networkWrapper = networkWrapper\n    }\n    \n    func execute(_ state: AppState, action: AppAction) -\u003e AnyPublisher\u003cMiddlewareAction\u003cAppAction, RouteWrapperAction\u003e, Never\u003e? {\n        switch action {\n        case .fetch:\n            return Deferred {\n                Future\u003cMiddlewareAction\u003cAppAction, RouteWrapperAction\u003e, Never\u003e { [weak self] promise in\n                    guard let self = self else { return }\n                    self.networkWrapper\n                        .request(path: URL(string: \"https://jsonplaceholder.typicode.com/users\")!, decode: [UserDTO].self)\n                        .sink { result in\n                            switch result {\n                            case .finished: break\n                            case .failure(let error):\n                                promise(.success(.performAction(.error(message: \"Something went wrong!\"))))\n                            }\n                        } receiveValue: { dto in\n                            promise(.success(.performAction(.updateUsers(dto))))\n                        }.store(in: \u0026self.cancelabels)\n                }\n            }.eraseToAnyPublisher()\n        default:\n            return nil\n        }\n    }\n}\n```\n\n`Content View`\n```swift\n@EnvironmentObject var store: Store\u003cAppState, AppAction, RouteWrapperAction\u003e\n    \nvar body: some View {\n    VStack {\n        ScrollView {\n            ForEach(store.state.users) { user in\n                HStack {\n                    VStack {\n                        Text(user.name)\n                            .padding(.leading, 16)\n                        Text(user.phone)\n                            .padding(.leading, 16)\n                    }\n                    Spacer()\n                }\n                Divider()\n            }\n        }\n        Spacer()\n        if store.state.isLoading {\n            Text(\"Loading\")\n        }\n        \n        if !store.state.errorMessage.isEmpty {\n            Text(LocalizedStringKey(store.state.errorMessage))\n        }\n        \n        Button {\n            store.dispatch(.fetch)\n        } label: {\n            Text(\"fetch users\")\n        }\n    }\n}\n```\n\nWhen reducer ended his job with action, our store check all added middlewares for some `Publishers` for curent `Action`, if Publisher not nil, `Store` runing that Publisher.\n\nYou can return action for reducer and change some data, return action for routing, return `.multiple` actions.\n\n```swift\ncase multiple([MiddlewareAction\u003cA, R\u003e])\n```\n\n#### You can return `Deferred Action`.\n\n```swift\npublic protocol DeferredAction {\n    associatedtype Action: AnyAction\n    func observe() -\u003e AnyPublisher\u003cAction, Never\u003e?\n    \n    func eraseToAnyDeferredAction() -\u003e AnyDeferredAction\u003cA\u003e\n}\n```\n\nIf you want route to Authorization, when your Session Provider send event about dead you session, you can use it `action`. All you need that conform to protocol `DeferredAction` you `class/struct` and erase it to `AnyDeferredAction` with generic `Action`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgre4ixin%2FReduxUI","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgre4ixin%2FReduxUI","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgre4ixin%2FReduxUI/lists"}