Swiftpack.co - StreetEasy/combine-ergonomics as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
StreetEasy/combine-ergonomics
A collection of helpful extensions and classes for working with the Combine framework
.package(url: "https://github.com/StreetEasy/combine-ergonomics.git", from: "v0.2.2")

CombineErgonomics

StreetEasy

This package contains useful extensions for Combine that make it easier to develop with and test.

To add this package to your project, go into your project settings and add the url https://github.com/StreetEasy/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.

Benefits

Attach handlers to Publishers

CombineErgonomics makes it easier to dispatch background tasks and handle their errors, with syntax inspired by PromiseKit. For example, consider this class that facilitates login:

import Combine

class LoginHelper {

    var store = Set<AnyCancellable>()

    func login() {
        let future = Future<User, Error> { promise in
            // network call to log in
        }
        future.subscribe(on: DispatchQueue.global())
            .sink { completion in
                if case .failure(let error) = completion {
                    // handle error
                }
            } receiveValue: { user in
                // handle user logged in, i.e. update UI
            }
            .store(in: &store)
    }
}

Can be reduced to:

import Combine
import CombineErgonomics

class LoginHelper { 

    func login() {
        let future = Future<User, Error> { promise in
            // network call to log in
        }
        future.done { user in
            // handle user logged in, i.e. update UI
        }.catch { error in
            //handle error
        }
    }
}

Chain together multiple Futures

Rather than using a chain of Combine.FlatMap, Futures can be neatly chained using the .then method.

import Combine
import CombineErgonomics

class LoginHelper { 

    func login() {
        let future = Future<User, Error> { promise in
            // network call to log in
        }
        future.then { user -> Future<ProfileImage, Error> in
            return ProfileImageHelper.fetchProfileImage(for: user)
        }.done { profileImage in 
            // update UI with new profile image
        }.catch { error in
            // handle error
        }
    }
}

Unit test published values

One 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.

import Combine
import CombineErgonomics
import XCTest

class LoginViewModel {
    @Published var user: User?
    @Published var isLoading = false

    func login() {
        self.isLoading = true
        NetworkHelper.login().done { user in
            self.user = user
        }.finally {
            self.isLoading = false
        }
    }
}

class LoginViewModelTests: XCTestCase { 

    func testLogin() {
        let loginViewModel = LoginViewModel()
        let loadingExpectation = XCTestExpectation(description: "View model starts loading, then finishes")
        loadingExpectation.expectedFulfillmentCount = 2
        var values: [Bool] = []
        let cancellable = loginViewModel.$isLoading.dropFirst(1).sink { isLoading in
            expectation.fulfill()
            values.append(isLoading)
        }
        loginViewModel.login()
        wait(for: [loadingExpectation], timeout: 1)
        XCTAssertEqual(values, [true, false])
        XCTAssertNotNil(loginViewModel.user)
    }
}

With 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.

import CombineErgonomicsTestExtensions
import XCTest

class LoginHelperTests: XCTestCase { 

    func testLogin() {
        let loginViewModel = LoginViewModel()
        let values = values(for: loginViewModel.$isLoading, expectedNumber: 2) {
            loginViewModel.login()
        }
        XCTAssertEqual(values, [true, false])
        XCTAssertNotNil(loginViewModel.user)
    }
}

Documentation

You can find more detailed documentation here.

GitHub

link
Stars: 3
Last commit: 7 hours ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Submit a free job ad (while I'm testing this). The analytics numbers for this website are here.

Release Notes

v0.2.2
11 weeks ago

Internal changes as a result of PR Comments.

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics