{"id":26246215,"url":"https://github.com/zillow/combine-ergonomics","last_synced_at":"2026-07-15T11:32:31.825Z","repository":{"id":45078273,"uuid":"445656439","full_name":"zillow/combine-ergonomics","owner":"zillow","description":null,"archived":false,"fork":false,"pushed_at":"2022-01-10T14:50:51.000Z","size":26045,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":7,"default_branch":"main","last_synced_at":"2026-04-21T14:55:40.887Z","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/zillow.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-01-07T21:25:06.000Z","updated_at":"2022-02-10T22:45:48.000Z","dependencies_parsed_at":"2022-09-07T22:11:31.292Z","dependency_job_id":null,"html_url":"https://github.com/zillow/combine-ergonomics","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/zillow/combine-ergonomics","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fcombine-ergonomics","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fcombine-ergonomics/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fcombine-ergonomics/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fcombine-ergonomics/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zillow","download_url":"https://codeload.github.com/zillow/combine-ergonomics/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fcombine-ergonomics/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35503614,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-15T02:00:06.706Z","response_time":131,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":"2025-03-13T13:17:38.266Z","updated_at":"2026-07-15T11:32:31.795Z","avatar_url":"https://github.com/zillow.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CombineErgonomics\n![Tests](https://github.com/zillow/combine-ergonomics/actions/workflows/build_and_test.yml/badge.svg)\n![](https://img.shields.io/badge/spm-compatible-green.svg)\n\nThis package contains useful extensions for Combine that make it easier to develop with and test.\n\nTo add this package to your project, go into your project settings and add the url `https://github.com/zillow/combine-ergonomics.git` to your Swift Packages. To use it, just `import CombineErgonomics` at the top of your swift files. To use the `XCTestCase` extensions, `import CombineErgonomicsTestExtensions`.\n\n## Benefits\n\n### Attach handlers to Publishers\n\nCombineErgonomics makes it easier to dispatch background tasks and handle their errors, with syntax inspired by [PromiseKit](https://github.com/mxcl/PromiseKit). For example, consider this class that facilitates login:\n```swift\nimport Combine\n\nclass LoginHelper {\n\n    var store = Set\u003cAnyCancellable\u003e()\n\n    func login() {\n        let future = Future\u003cUser, Error\u003e { promise in\n            // network call to log in\n        }\n        future.subscribe(on: DispatchQueue.global())\n            .sink { completion in\n                if case .failure(let error) = completion {\n                    // handle error\n                }\n            } receiveValue: { user in\n                // handle user logged in, i.e. update UI\n            }\n            .store(in: \u0026store)\n    }\n}\n```\n\nCan be reduced to:\n\n```swift\nimport Combine\nimport CombineErgonomics\n\nclass LoginHelper { \n\n    func login() {\n        let future = Future\u003cUser, Error\u003e { promise in\n            // network call to log in\n        }\n        future.done { user in\n            // handle user logged in, i.e. update UI\n        }.catch { error in\n            //handle error\n        }\n    }\n}\n```\n\n### Chain together multiple Futures\n\nRather than using a chain of `Combine.FlatMap`, `Future`s can be neatly chained using the `.then` method.\n\n```swift\nimport Combine\nimport CombineErgonomics\n\nclass LoginHelper { \n\n    func login() {\n        let future = Future\u003cUser, Error\u003e { promise in\n            // network call to log in\n        }\n        future.then { user -\u003e Future\u003cProfileImage, Error\u003e in\n            return ProfileImageHelper.fetchProfileImage(for: user)\n        }.done { profileImage in \n            // update UI with new profile image\n        }.catch { error in\n            // handle error\n        }\n    }\n}\n```\n\n### Unit test published values\n\nOne common pattern used in reactive programming, and especially in MVVM app architecture is binding the view's state to observable values on a view model. Testing these view model properties can be a bit messy when asynchronous code is involved.\n\n```swift\nimport Combine\nimport CombineErgonomics\nimport XCTest\n\nclass LoginViewModel {\n    @Published var user: User?\n    @Published var isLoading = false\n\n    func login() {\n        self.isLoading = true\n        NetworkHelper.login().done { user in\n            self.user = user\n        }.finally {\n            self.isLoading = false\n        }\n    }\n}\n\nclass LoginViewModelTests: XCTestCase { \n\n    func testLogin() {\n        let loginViewModel = LoginViewModel()\n        let loadingExpectation = XCTestExpectation(description: \"View model starts loading, then finishes\")\n        loadingExpectation.expectedFulfillmentCount = 2\n        var values: [Bool] = []\n        let cancellable = loginViewModel.$isLoading.dropFirst(1).sink { isLoading in\n            expectation.fulfill()\n            values.append(isLoading)\n        }\n        loginViewModel.login()\n        wait(for: [loadingExpectation], timeout: 1)\n        XCTAssertEqual(values, [true, false])\n        XCTAssertNotNil(loginViewModel.user)\n    }\n}\n```\n\nWith the test extensions, the login test case can be reduced to something much more readable, allowing you to focus on the actual logic being tested.\n\n```swift\nimport CombineErgonomicsTestExtensions\nimport XCTest\n\nclass LoginHelperTests: XCTestCase { \n\n    func testLogin() {\n        let loginViewModel = LoginViewModel()\n        let values = values(for: loginViewModel.$isLoading, expectedNumber: 2) {\n            loginViewModel.login()\n        }\n        XCTAssertEqual(values, [true, false])\n        XCTAssertNotNil(loginViewModel.user)\n    }\n}\n```\n\n## Documentation\n\nYou can find [more detailed documentation here](http://combine-ergonomics-framework-docs.s3-website-us-east-1.amazonaws.com/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzillow%2Fcombine-ergonomics","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzillow%2Fcombine-ergonomics","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzillow%2Fcombine-ergonomics/lists"}