{"id":15527097,"url":"https://github.com/kvs-coder/swiftbloc","last_synced_at":"2025-10-17T03:29:20.375Z","repository":{"id":37539817,"uuid":"342932284","full_name":"kvs-coder/SwiftBloc","owner":"kvs-coder","description":"SwiftBloc. A state management library based on SwiftUI and Combine to separate presentation layer from business logic.","archived":false,"fork":false,"pushed_at":"2022-09-07T11:45:27.000Z","size":171,"stargazers_count":53,"open_issues_count":1,"forks_count":8,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-23T12:14:06.063Z","etag":null,"topics":["bloc","combine","event-driven","flutter","flutterbloc","ios","reactive-programming","rxswift","state-management","swift","swiftui"],"latest_commit_sha":null,"homepage":"https://medium.com/codex/swift-business-logic-component-bloc-e54aca3f5b9f","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/kvs-coder.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-02-27T18:46:52.000Z","updated_at":"2025-03-22T20:08:33.000Z","dependencies_parsed_at":"2022-08-08T20:31:03.335Z","dependency_job_id":null,"html_url":"https://github.com/kvs-coder/SwiftBloc","commit_stats":null,"previous_names":["victorkachalov/swiftbloc"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kvs-coder%2FSwiftBloc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kvs-coder%2FSwiftBloc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kvs-coder%2FSwiftBloc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kvs-coder%2FSwiftBloc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kvs-coder","download_url":"https://codeload.github.com/kvs-coder/SwiftBloc/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250430595,"owners_count":21429324,"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":["bloc","combine","event-driven","flutter","flutterbloc","ios","reactive-programming","rxswift","state-management","swift","swiftui"],"created_at":"2024-10-02T11:04:25.646Z","updated_at":"2025-10-17T03:29:15.331Z","avatar_url":"https://github.com/kvs-coder.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SwiftBloc\n\n[![Version](https://img.shields.io/cocoapods/v/SwiftBloc.svg?style=flat)](https://cocoapods.org/pods/SwiftBloc)\n[![Platform](https://img.shields.io/cocoapods/p/SwiftBloc.svg?style=flat)](https://cocoapods.org/pods/SwiftBloc)\n\n## About\n\nInspired from a really great Flutter package [flutter_bloc](https://pub.dev/packages/flutter_bloc) this SwiftUI library brings the state management with a BloC (Buisiness logic component) approach\nto separate views and buisiness logic. With the help of Apple Library \"Combine\" the state management is handled with the reactive approach.\n\nRequires iOS 13.0+ and MacOS 10.15+\n\n## Start\n\nAt first you need to decide what approach is more suitable for your app. \n\nIf you prefer to make something simple and make changes without depending on what event is currently hapening you could use a **Cubit** class to create your child cubit and handle state there.\n\nIf you would need more complex implementation to track events and map them into states then **Bloc** class is the choice.\n\nIn both scenarios you may also need to create inside your custom **View** structure a **BlocView** instance which will accept your newly created cubit/bloc in the initializer and also will require a **@ViewBuilder** builder function to be provided as well. The idea of the **BlocView** is to handle rebuilding your views inside the **builder** callback based on the current state. Whenever the state is changed your view gets rebuild. \n\nYou can manipaulate with state as you want. In addition you can also provide to the  **BlocView** constructor a custom **action** callback which will let you to add some side logic or view behavior without returning any **View** conforming object.\n\nBy default all changes are tracked by a shared instance of **BlocObserver** and currently only make console printing. You can always set a custom observer for the shared instance and override open methods as you wish.\n\nFor convinience you can use a ruby script *bloc_template.rb*. You need to provide additional parameters for executing the script.\n- path\n- class_name\n- type\n\nExample:\n\n```ruby\n# creates a cubit\nruby bloc_template /MY_PROJECT/MY_CUBIT_FOLDER Counter cubit\n```\n\n```ruby\n# creates a bloc\nruby bloc_template /MY_PROJECT/MY_BLOC_FOLDER Counter bloc\n```\n\n### Cubit\n\nIf you go with **Cubit** first you need to create a child class. The generic type **State** can be any type which conforms **Equitable** protocol.\n\nWith the super constructor you can provide the initial state. \n\nIn order to emit a new state you need to use **emit(state:)** method\n\nAs a simple example the **CounterCubit** (the beloved one standar app from Flutter new project)\n\n```swift \nimport SwiftBloc\n\nclass CounterCubit: Cubit\u003cInt\u003e {\n    init() {\n        super.init(state: 0)\n    }\n\n    func increment() {\n        emit(state: state + 1)\n    }\n    func decrement() {\n         emit(state: state - 1)\n    }\n}\n```\n\nNext you need to use it in your views.\n\nSince our **State**generic state is **Int** we can directly access to an integer getter **state** and display in in the text.\n\nNext, inside your **body** property create a **BlocView** with some content:\n\n```swift\nvar body: some View {\n    BlocView(builder: { (cubit)  in\n        VStack {\n            Button(action: {\n                cubit.increment()\n            }, label: {\n                Text(\"Increment\")\n            })\n            Button(action: {\n                cubit.decrement()\n            }, label: {\n                Text(\"Decrement\")\n            })\n            Text(\"Count: \\(cubit.state)\")\n        }\n    }, cubit: CounterCubit())\n}\n```\n\nIn this case you may use **cubit** directly from a **builder**. Moreover this approach will let to use your **cubit** instance as an **@EnvironmentObject** so every child **view** inside your **builder** function will get the instance of **cubit** without a need \"to drill\" through the whole view tree.\n\n### Bloc\n\nIf you go with **Bloc** first you need also to create a child class. The generic types **Event** and **State** can be any types which conforms **Equitable** protocol.\n\nWith the super constructor you can provide the initial state. \n\nThe difference between **Cubit** and **Bloc** although the **Bloc** is a child class of the **Cubit** ist the the **Bloc** uses events to map them into states.\n\nAs a simple example the **CounterBloc**\n\n```swift\nimport SwiftBloc\n\nenum CounterEvent {\n    case increment\n    case decrement\n}\n\nstruct CounterState: Equatable {\n    let count: Int\n\n    func copyWith(count: Int?) -\u003e CounterState {\n        CounterState(count: count ?? self.count)\n    }\n}\n\nclass CounterBloc: Bloc\u003cCounterEvent, CounterState\u003e {\n    init() {\n        super.init(initialState: CounterState(count: 0))\n    }\n\n    override func mapEventToState(event: CounterEvent) -\u003e CounterState {\n        switch event {\n        case .increment:\n            return state.copyWith(count: state.count + 1)\n        case .decrement:\n            return state.copyWith(count: state.count - 1)\n        }\n    }\n}\n```\n\nThe idea is, that everything what is happening in the app are events. Based on that you can call your event as you want, in the example the Enum is used (but you can also use Classes as well, but keep in mind to conform **Equatable** protocol).\n\nIf there are event, then there should be something what will tell about the current app state after the appropriate event.\n\nYou need to specify your state model for this. You can also use Classes with inheritance especially if you have some complicated states (and remebemer **Equatable**)\n\nNow it is time to implement the child **Bloc** class.\n\nYou need to have a constructor where you specify the initial **State** value.\n\nTHE MOST IMPORTANT PART HERE - is to override the **mapEventToState(event:)** method. Without it nothing will work...\n\nThe goal of this method is to **transform events to states**.\n\nSo based on incoming events, the states are generated. You may create new objects of state using **CounterState** or your can create a method to copy the current state and provide changes to it.\n\nThen the new state will be delivered to your **BlocView** and your view will be rebuilt automatically!\n\nNow let's see what is the view looks like:\n\n```swift\nimport SwiftBloc\n\nstruct BlocContentView: View {\n    var body: some View {\n        NavigationView {\n            BlocView(builder: { (bloc) in\n                let isPresented = Binding.constant(bloc.state.count \u003c -6)\n                CounterView()\n                    .alert(isPresented: isPresented) {\n                        Alert(\n                            title: Text(\"Hi\"),\n                            message: Text(\"Message\"),\n                            dismissButton: .cancel {\n                                for _ in 0..\u003c6 {\n                                    bloc.add(event: .increment)\n                                }\n                            }\n                        )\n                    }\n            }, action: { (bloc) in\n                print(bloc.state.count)\n            }, base: CounterBloc())\n            .navigationBarTitle(Text(\"Bloc\"), displayMode: .inline)\n        }\n    }\n}\n\nstruct CounterView: View {\n    @EnvironmentObject var bloc: CounterBloc\n\n    var body: some View {\n        if bloc.state.count \u003e 5 {\n            LimitView()\n        } else {\n            OperationView()\n        }\n    }\n}\n\nstruct LimitView: View {\n    @EnvironmentObject var bloc: CounterBloc\n\n    var body: some View {\n        VStack {\n            Text(\"Hooora\")\n            Button(action: {\n                for _ in 0..\u003c6 {\n                    bloc.add(event: .decrement)\n                }\n            }, label: {\n                Text(\"Reset\")\n            })\n        }\n    }\n}\n\nstruct OperationView: View {\n    @EnvironmentObject var bloc: CounterBloc\n\n    var body: some View {\n        VStack {\n            Button(action: {\n                bloc.add(event: .increment)\n            }, label: {\n                Text(\"Send Increment event\")\n            })\n            Button(action: {\n                bloc.add(event: .decrement)\n            }, label: {\n                Text(\"Send Decrement event\")\n            })\n            Text(\"Count: \\(bloc.state.count)\")\n        }\n    }\n}\n```\n\nThe **Bloc** class is monitoring the changes of the **event** property via **@PublishedSubject** property wrapper. This **bloc** instance is set by default as **@EnvironmentObject** and will be available for all child views if needed.\n\n### BlocTest\n\nYou may want to test your Blocs to be sure that the awaited states are actually happening if appropriate events passed in.\n\n**BlocTest.execute** will expect to have four callbacks:\n\n- **build** (create a Bloc instance inside the closure)\n- **act** (provide a sequence of events)\n- **expect** (set awaited states)\n- **verify** (handle information about equality of expected and real states)\n\n```swift\nfinal class SwiftBlocTests: XCTestCase {\n    func testCounterBlocInitial() {\n        BlocTest.execute(build: {\n            MockCounterBloc()\n        }, act: { (_) in\n            // DO NOTHING\n        }, expect: {\n            [\n                MockCounterState(count: 0)\n            ]\n        }, verify: { areEqual, message in\n            XCTAssert(areEqual, message)\n        })\n    }\n    func testCounterBlocIncrement() {\n        BlocTest.execute(build: {\n            MockCounterBloc()\n        }, act: { (bloc) in\n            bloc.add(event: .increment)\n            bloc.add(event: .increment)\n        }, expect: {\n            [\n                MockCounterState(count: 0),\n                MockCounterState(count: 1),\n                MockCounterState(count: 2)\n            ]\n        }, verify: { areEqual, message in\n            XCTAssert(areEqual, message)\n        })\n    }\n    func testCounterBlocDecrement() {\n        BlocTest.execute(build: {\n            MockCounterBloc()\n        }, act: { (bloc) in\n            bloc.add(event: .decrement)\n            bloc.add(event: .decrement)\n        }, wait: 3.0, expect: {\n            [\n                MockCounterState(count: 0),\n                MockCounterState(count: -1),\n                MockCounterState(count: -2)\n            ]\n        }, verify: { areEqual, message in\n            XCTAssert(areEqual, message)\n        })\n    }\n}    \n```\n\n## Example\n\nTo run the example project, clone the repo, and run `pod install` from the Example directory first.\n\n## Requirements\n\nThe library supports iOS 13 \u0026 above. \n\n## Installation\n\n### Cocoapods\n\nSwiftBloc is available through [CocoaPods](https://cocoapods.org). To install\nit, simply add the following line to your Podfile:\n\n```ruby\npod 'SwiftBloc'\n```\n\nor \n\n```ruby\npod 'SwiftBloc', '~\u003e 1.0.3'\n```\n\n### Swift Package Manager\n\nTo install it, simply add the following lines to your Package.swift file\n(or just use the Package Manager from within XCode and reference this repo):\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/VictorKachalov/SwiftBloc.git\", from: \"1.0.3\")\n]\n```\n\n### Carthage\n\nAdd the line in your cartfile \n\n```ruby\ngithub \"VictorKachalov/SwiftBloc\" \"1.0.3\"\n```\n\n## Author\n\nVictor Kachalov, victorkachalov@gmail.com\n\n## License\n\nSwiftBloc is available under the MIT license. See the LICENSE file for more info.\n\n## Sponsorhip\nIf you think that my repo helped you to solve the issues you struggle with, please don't be shy and sponsor :-)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkvs-coder%2Fswiftbloc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkvs-coder%2Fswiftbloc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkvs-coder%2Fswiftbloc/lists"}