{"id":936,"url":"https://github.com/roytornado/Flow-iOS","last_synced_at":"2025-07-30T19:33:11.015Z","repository":{"id":56911474,"uuid":"70889771","full_name":"roytornado/Flow-iOS","owner":"roytornado","description":"Make your logic flow and data flow clean and human readable","archived":false,"fork":false,"pushed_at":"2017-08-16T07:53:22.000Z","size":48,"stargazers_count":21,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-04-28T14:21:21.985Z","etag":null,"topics":["callback-hell","chain-of-responsibility","design-pattern","flow","nsoperation","nsoperationqueue","operation","swift"],"latest_commit_sha":null,"homepage":null,"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/roytornado.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":"2016-10-14T08:25:25.000Z","updated_at":"2024-02-11T23:35:06.000Z","dependencies_parsed_at":"2022-08-21T03:20:18.163Z","dependency_job_id":null,"html_url":"https://github.com/roytornado/Flow-iOS","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roytornado%2FFlow-iOS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roytornado%2FFlow-iOS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roytornado%2FFlow-iOS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roytornado%2FFlow-iOS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roytornado","download_url":"https://codeload.github.com/roytornado/Flow-iOS/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":215182659,"owners_count":15840914,"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":["callback-hell","chain-of-responsibility","design-pattern","flow","nsoperation","nsoperationqueue","operation","swift"],"created_at":"2024-01-05T20:15:35.006Z","updated_at":"2024-08-13T13:30:20.740Z","avatar_url":"https://github.com/roytornado.png","language":"Swift","readme":"# Flow\n\n[![Version](https://img.shields.io/cocoapods/v/Flow.svg?style=flat)](http://cocoapods.org/pods/Flow)\n[![License](https://img.shields.io/cocoapods/l/Flow.svg?style=flat)](http://cocoapods.org/pods/Flow)\n[![Platform](https://img.shields.io/cocoapods/p/Flow.svg?style=flat)](http://cocoapods.org/pods/Flow)\n\n## What's Flow\nFlow is an utility/ design pattern that help developers to write simple and readable code.\nThere are two main concerns:\n`Flow of operations and Flow of data`\n\nBy using Flow, we should able to achieve the followings:\n- Logics / operations can be reused easily\n- The logic flows are readable by anyone (including the code reviewers)\n- Each line of code is meaningfully and avoid ambiguous keywords\n- No more callback hell for complicated async operations\n- Debuggable both at development and production stage\n\nFlow is referencing Composite pattern (https://en.wikipedia.org/wiki/Composite_pattern) and\nChain-of-responsibility pattern (which including Command pattern) (https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern)\n\nSo, we encapsulate operations as objects which can be chained using tree structures. Each operation is independent but able to be used with another one if the data required by the operations exist.\n\nHere is an example for simple usage:\n```swift\n@IBAction func simpleChainedFlow() {\n    Flow()\n      .add(operation: SimplePrintOp(message: \"hello world\"))\n      .add(operation: SimplePrintOp(message: \"good bye\"))\n      .setWillStartBlock(block: commonWillStartBlock())\n      .setDidFinishBlock(block: commonDidFinishBlock())\n      .start()\n  }\n```\nIn these 5 lines of code, we can know that two operations will be executed in serial and able to do something before and after the operations.\n\n## Naming\nTo make the logic readable, it is important to make the operation's name meaningfully. It is developer's responsibility to make a good name. Also, the name also determine the degree of reusable of code.\ne.g. If you create an operation named: `SimplePrintOp`, it should contain only the code to print the message associated with it. You should NOT do anything out of the context of the name. Such as sending the message to server / write to file.\n\nAlso, all operations made for Flow should share a common suffix (e.g. Op) so all developers can know that there are operations that ready for reuse.\n\n## Grouped Operations\nYou can run a batch of operations using `FlowArrayGroupDispatcher`.\n```swift\nFlow()\n      .setDataBucket(dataBucket: [\"images\": [\"a\", \"b\", \"c\", \"d\"]])\n      .addGrouped(operationCreator: UploadSingleImageOp.self, dispatcher: FlowArrayGroupDispatcher(inputKey: \"images\", outputKey: \"imageURLs\", maxConcurrentOperationCount: 3, allowFailure: false))\n      .start()\n```\nFlowArrayGroupDispatcher will dispatcher the targeted array in the data bucket to created operations and pass them the required data and collect them afterwards.\n\n```swift\nimport Flow_iOS\nclass UploadSingleImageOp: FlowOperation, FlowOperationCreator {\n  \n  static func create() -\u003e FlowOperation {\n    return UploadSingleImageOp()\n  }\n  \n  override var primaryInputParamKey: String { return \"targetImageForUpload\" }\n  override var primaryOutputParamKey: String { return \"uploadedImageUrl\" }\n  \n  override func mainLogic() {\n    guard let image: String = getData(name: primaryInputParamKey) else { return }\n    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {\n      self.log(message: \"simulation of upload single image callback\")\n      self.setData(name: self.primaryOutputParamKey, value: \"url_of_\" + image)\n      self.finishSuccessfully()\n    }\n    startWithAsynchronous()\n  }\n}\n\n```\nFor the above example, FlowArrayGroupDispatcher will create a group of UploadSingleImageOp based on the array size of `images` in data bucket. As UploadSingleImageOp declares `targetImageForUpload` as it's input key and `uploadedImageUrl` as it's output key. FlowArrayGroupDispatcher will create temporary data bucket for each UploadSingleImageOp and contains `targetImageForUpload` inside. If the operation is succeed, FlowArrayGroupDispatcher will collect the object keyed with `uploadedImageUrl` and put into the result array `imageURLs`.\n\nIn such design, UploadSingleImageOp can be `reused as single operation or grouped operation`.\n\nYou can also set the `maxConcurrentOperationCount (optional, default = 3)` to control whether the operations are executed on one by one or in batch.\nIf `allowFailure (optional, default = false)` is set to true, the Flow will continue to run even some / all operations in the group are failed. Therefore, the output array may be shorter than the input array or even empty.\n\n## Cases\nFlow allow simple cases handling. For example:\n```swift\n@IBAction func demoCases() {\n    Flow()\n      .setDataBucket(dataBucket: [\"images\": [\"a\", \"b\", \"c\", \"d\", 1], \"target\": \"A\"])\n      .add(operation: SimplePrintOp(message: \"Step1\"))\n      .add(operation: SimplePrintOp(message: \"Step2A1\"), flowCase: FlowCase(key: \"target\", value: \"A\"))\n      .add(operation: SimplePrintOp(message: \"Step2A2\"))\n      .add(operation: SimplePrintOp(message: \"Step2B1\"), flowCase: FlowCase(key: \"target\", value: \"B\"))\n      .add(operation: SimplePrintOp(message: \"Step2B2\"))\n      .combine()\n      .add(operation: SimplePrintOp(message: \"Step3\"))\n      .setWillStartBlock(block: commonWillStartBlock())\n      .setDidFinishBlock(block: commonDidFinishBlock())\n      .start()\n  }\n```\nAfter `Step1` is finished, the Flow will run `Step2A1` branch or `Step2B1` branch depend on the value of `target` in data bucket. And `combine` is used to combine all cases back to a single node `Step3`.\n\nTo make the blueprint readable, `nested case is NOT supported`.\nAlso, the type of case value must be `String`.\n\n## Data Handling\n```swift\nimport Flow_iOS\n\nclass MockAsyncLoginOp: FlowOperation {\n  override func mainLogic() {\n    guard let email: String = getData(name: \"email\") else { return }\n    guard let password: String = getData(name: \"password\") else { return }\n    \n    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {\n      self.log(message: \"simulation of login callback\")\n      if email == \"test@gmail.com\" \u0026\u0026 password == \"123456\" {\n        let mockAccessToken = \"sdaftadagasg\"\n        self.setData(name: \"accessToken\", value: mockAccessToken)\n        self.finishSuccessfully()\n      } else {\n        let error = NSError(domain: \"No such account\", code: 404, userInfo: nil)\n        self.finishWithError(error: error)\n      }\n    }\n    startWithAsynchronous()\n  }\n}\n```\nIn MockAsyncLoginOp in the example, it require two input data from data bucket (`email` and `password`). The Flow will check if `the data exist` and if `the data type is correct` (i.e. they must be `String` for this case). If no data is found with correct type, the operation is marked as failure and the Flow will stop.\nYou can request any type you want. For example, you have a class named \"LoginData\" in your project.\n```swift\nguard let loginData: LoginData = getData(name: \"loginData\") else { return }\n```\n\n## Making your Operations\nMaking operation is easy:\n1) Pick a `good name`\n2) Inherit `FlowOperation`\n3) Put your logic inside `mainLogic`\n4) For synchronized operation: call `finishSuccessfully` or `finishWithError` based on the result\n5) For asynchronized operation: call `startWithAsynchronous` at the end of `mainLogic` after starting your async call\n6) use `log` to record your debug logs\n7) extends `FlowOperationCreator` to make the operation to use in `Group` And override `primaryInputParamKey` and `primaryOutputParamKey`\n\nSome examples:\n```swift\nclass SimplePrintOp: FlowOperation {\n  var message: String!\n  \n  init(message: String) {\n    self.message = message\n  }\n  \n  override func mainLogic() {\n    log(message: message)\n    finishSuccessfully()\n  }\n}\n\nclass MockAsyncLoadProfileOp: FlowOperation {\n  override func mainLogic() {\n    guard let accessToken: String = getData(name: \"accessToken\") else { return }\n    \n    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {\n      self.log(message: \"simulation of success load profile callback\")\n      self.setData(name: \"profileRefreshDate\", value: Date())\n      self.finishSuccessfully()\n    }\n    startWithAsynchronous()\n  }\n}\n\nclass UploadSingleImageOp: FlowOperation, FlowOperationCreator {\n  \n  static func create() -\u003e FlowOperation {\n    return UploadSingleImageOp()\n  }\n  \n  override var primaryInputParamKey: String { return \"targetImageForUpload\" }\n  override var primaryOutputParamKey: String { return \"uploadedImageUrl\" }\n  \n  override func mainLogic() {\n    guard let image: String = getData(name: primaryInputParamKey) else { return }\n    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {\n      self.log(message: \"simulation of upload single image callback\")\n      self.setData(name: self.primaryOutputParamKey, value: \"url_of_\" + image)\n      self.finishSuccessfully()\n    }\n    startWithAsynchronous()\n  }\n}\n\n```\n## Callbacks\nYou can set `WillStartBlock` and `DidFinishBlock` to get notified before or after the Flow run.\nThey are called in `main thread` so you can do your UI changes.\nWith the Flow instance in the block, you can get `dataBucket: [String: Any]`, `isSuccess: Bool` and `error: Error?` and do your handling.\n\nIt's recommended to make common handling blocks which can further simplify your blueprint.\n```swift\n  func commonWillStartBlock(block: FlowWillStartBlock? = nil) -\u003e FlowWillStartBlock {\n    let result: FlowWillStartBlock = {\n      flow in\n      block?(flow)\n      self.summaryTextView.text = \"Flow Starting...\"\n    }\n    return result\n  }\n  \n  func commonDidFinishBlock(block: FlowDidFinishBlock? = nil) -\u003e FlowDidFinishBlock {\n    let result: FlowDidFinishBlock = {\n      flow in\n      block?(flow)\n      self.summaryTextView.text = flow.generateSummary()\n    }\n    return result\n  }\n\n```\n\n\n## Logging\nThis my most favourite feature when making Flow. It's always hard for developer to trace the console log as there are too many unwanted logs in the console. Even worse in a serious of async operations.\nFlow will capture all the logs you sent within the opertions and generate a summary for you at the end.\nCall `flow.generateSummary()` in the finish block.\n\nFor example:\n```\n====== Flow Summary ======\n4:17:18 PM [DataBucket] start with:\npassword: 123456\nemail: test@gmail.com\n4:17:18 PM [MockAsyncLoginOp] WillStart\n4:17:19 PM [MockAsyncLoginOp] simulation of login callback\n4:17:19 PM [DataBucket] add for accessToken: sdaftadagasg\n4:17:19 PM [MockAsyncLoginOp] DidFinish: successfully\n4:17:19 PM [MockAsyncLoadProfileOp] WillStart\n4:17:20 PM [MockAsyncLoadProfileOp] simulation of success load profile callback\n4:17:20 PM [DataBucket] add for profileRefreshDate: 2017-07-13 08:17:20 +0000\n4:17:20 PM [MockAsyncLoadProfileOp] DidFinish: successfully\n4:17:20 PM [DataBucket] end with:\nprofileRefreshDate: 2017-07-13 08:17:20 +0000\nemail: test@gmail.com\naccessToken: sdaftadagasg\npassword: 123456\nFlow isSuccess: true\n======    Ending    ======\n\n====== Flow Summary ======\n4:17:06 PM [DataBucket] start with:\npassword: 123456\nemail_address: test@gmail.com\n4:17:06 PM [MockAsyncLoginOp] WillStart\n4:17:06 PM [MockAsyncLoginOp] DidFinish: withInsufficientInputData: can't find \u003cemail\u003e in data bucket\n4:17:06 PM [DataBucket] end with:\npassword: 123456\nemail_address: test@gmail.com\nFlow isSuccess: false\n======    Ending    ======\n```\nYou can trace the data changes, how the operations run in one place. You can send the summary string to your server if needed.\n\n## Why not RxSwift?\nSurely RXsSwift is much more powerful in some aspects.\nBUT I think it's always good if we can make our code: `Simple and Human readable`\nWith `Flow`, even code reviewers and non-programmer can understand your logic in the blueprint.\n\n## Requirements\nSwift 3.2\niOS 8.0\n\n## Installation\n\nFlow is available through [CocoaPods](http://cocoapods.org):\n```ruby\npod \"Flow-iOS\"\n```\n\nImport:\n```swift\nimport Flow_iOS\n```\n\n## Author\n\nRoy Ng, roytornado@gmail.com @ Redso, https://www.redso.com.hk/\n\nLinkedin: https://www.linkedin.com/in/roy-ng-19427735/\n\n## License\n\nFlow is available under the MIT license. See the LICENSE file for more info.\n","funding_links":[],"categories":["Concurrency"],"sub_categories":["Linter","Other free courses"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froytornado%2FFlow-iOS","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froytornado%2FFlow-iOS","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froytornado%2FFlow-iOS/lists"}