Combine, backward deployment and linux

Hello Swift Community, I'd like to share my recent work. I believe it eliminate the major barrier of integrating with Combine.

Motivation

Combine is a great tool for processing async value. With open source reimplementaions like CombineX and OpenCombine, one can easily adopt Combine for their application without system limitation.

However, it's not that easy for library author. Third party Combine means extra dependency and complexity, increased package size, and the worst, incompatibility with SwiftUI.

As a result, the majority of library that supports Combine doesn't benefit from Combine at all. They still use callbacks, delegate, notification center, KVO and target/action to build the core module. Then build a sweet Combine overlay on it. Of course, the overlay don't support open source Combine (as well as backward deployment and Linux).

Different Combine implementations also means incompatible ecosystem. It's really unfortunate to have 3 ecosystem which built on almost the same Combine API, but incompatible with each other. A library author have to either choose one implementation and lose users from other ecosystem, or create 3 package on each implementations.

From this perspective, Combine is even worse than RxSwift. Because there is only one RxSwift that available on all platforms. As a library author, you definitely don't want to maintain GRDBCombine, GRDBCombineX and GRDBOpenCombine at the same time. So how can we change?

Proposed solution

Introducing CXShim, a virtual Combine interface. It allows user to choose concrete Combine implementation at build time (by setting environment variable for now). It support system Combine, CombineX, OpenCombine, and will support any future Combine reimplementations.

A package built on CXShim is called Combine Compatible Package. It's not another ecosystem. Instead it combine 3 ecosystem as one. You can write one single package and support 3 ecosystem.

Additionally, on Apple's platforms, system Combine is used by default. Which means a Combine Compatible Package can be used with SwiftUI without additional setup, just like a normal package built on system Combine.

Detailed design

CXShim basically re-export all symbols from appropriate Combine implementation, and provides unified namespace.

Combine's types, methods and properties needs to be accessed through cx namespace in order to prevent conflicts.

import CXShim

Optional.CX.Publisher(1)

URLSession.shared.cx.dataTaskPublisher(for: url)
// It works with both Combine and CombineX.
// This method is under cx namespace because `URLSession.dataTaskPublisher` 
// can't be overloaded and always use system Combine.

The test suit of open source Combine also runs against system Combine. OpenCombineTests use tons of compilation conditions and typealias. In contrast, CombineXTests (built on CXShim) does't use any. CXShim does the dirty work.

Example

Here is the depencency graph of my personal project:

Note how LyricsKit is used by

Linux and iOS app doesn't need any extra setup, just like using normal packages. The macOS app need to set environment variable for backward deployment however.

Future direction

The package configuration can be implemented using native package flavors feature, instead of ad-hoc environment variable.

7 Likes