Skip to content

Combine Migration Guide

ddddxxx edited this page Apr 26, 2021 · 8 revisions

One of our goals is to let you use CombineX just like Apple Combine. However, due to the language limitation, replaceing every import Combine with import CombineX is not sufficient. for example, URLSession.dataTaskPublisher(for:) can't be overloaded and always use Apple Combine. Here is the complete guide for migrating from Combine to CombineX.

It's recommanded to use CXShim instead of hard coded CombineX so your library can support both CombineX and Apple Combine.

Preparation

Copy and paste the following code at the end of your Package.swift. It will be used in the later steps

enum CombineImplementation {
    
    case combine
    case combineX
    case openCombine
    
    static var `default`: CombineImplementation {
        #if canImport(Combine)
        return .combine
        #else
        return .combineX
        #endif
    }
    
    init?(_ description: String) {
        let desc = description.lowercased().filter { $0.isLetter }
        switch desc {
        case "combine":     self = .combine
        case "combinex":    self = .combineX
        case "opencombine": self = .openCombine
        default:            return nil
        }
    }
    
    var swiftSettings: [SwiftSetting] {
        switch self {
        case .combine:      return [.define("USE_COMBINE")]
        case .combineX:     return [.define("USE_COMBINEX")]
        case .openCombine:  return [.define("USE_OPEN_COMBINE")]
        }
    }
}

extension Optional where Wrapped: RangeReplaceableCollection {
    
    mutating func append(contentsOf newElements: [Wrapped.Element]) {
        if newElements.isEmpty { return }
        
        if let wrapped = self {
            self = wrapped + newElements
        } else {
            self = .init(newElements)
        }
    }
}

import Foundation

extension ProcessInfo {
    
    var combineImplementation: CombineImplementation {
        return environment["CX_COMBINE_IMPLEMENTATION"].flatMap(CombineImplementation.init) ?? .default
    }
}

Supported Platforms

You package is likely requires macOS 10.15 / iOS 13. With CXShim, it automatically support older version and linux:

  let package = Package(
      name: "MyAwesomeLibrary",
      // no requirements needed when using CombineX
-     platforms: [
-         .macOS(.v10_15), 
-         .iOS(.v13),
-         .tvOS(.v13),
-         .watchOS(.v6),
-     ],
      products: [
  
  ...
  
  // at the end of Package.swift
  
  // you probably still need system requirements when using system Combine
+ if ProcessInfo.processInfo.combineImplementation == .combine {
+     package.platforms = [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)]
+ }

Typealias

Foundation somehow export Published and Observableobject from Combine. It introduces ambiguity:

import Foundation
import CombineX

class A: ObservableObject {}
// ❗️ 'ObservableObject' is ambiguous for type lookup in this context
// Foundation.ObservableObject or CombineX.ObservableObject?

CXShim export desired type. You just need to export them in your module:

// create Typealias.swift

+ import CXShim
+ 
+ public typealias Published = CXShim.Published
+ public typealias ObservableObject = CXShim.ObservableObject

// Now you can use it normally

class A: ObservableObject {}

The cx Namespace

As described above, some Combine extensions on non-Combine type are not overloadable. They need to be accessed through cx namespace:

let pub1 = [1, 2, 3].cx.publisher
//                 ^    ^
//   Swift's world |    | Combine's world

let pub2 = URLSession.shared.cx.dataTaskPublisher(for: request)
//                         ^    ^
//      Foundation's world |    | Combine's world

You also need .cx to pass non-Combine type as Combine protocol:

pub.receive(on: DispatchQueue.main.cx)
//            ^ pass as Scheduler

pub.decode(type: Model.self, decoder: JSONDecoder().cx)
//                                  ^ pass as TopLevelDecoder
// practically `JSONDecoder` can conform to `CombineX.TopLevelDecoder`, but we don't do this for consistency.

Theres's also CX namespace for types:

let pub: Optional.CX.Publisher = .init(42)
//              ^    ^
//   Swift Type |    | Combine Type

However it becomes a little bit tricky on NSObject Subclasses. The compiler can't distinguish between NSObject.CX and, say, Timer.CX. In this case, we have CXWrapper:

CXWrappers.Timer.publish(...)
// CXWrappers.Timer is equivalent to Timer.CX, but without ambiguity issue

let dt: CXWrappers.DispatchQueue.SchedulerTimeType.Stride = .seconds(1)
// CXWrappers.DispatchQueue conforms to Schedular and have its SchedulerTimeType etc.

For consistency reason, you can still use CXWrappers on non-NSObject types. It's recommanded to use CXWrappers instead of CX.

- let pub: Optional.CX.Publisher = .init(42)
+ let pub: CXWrappers.Optional.Publisher = .init(42)

Combine Implementation Specific Code

Although CXShim have dealt with most of the differences between underlying Combine, sometime you still want to handle the differences yourself. You can do it with predefined compilation conditions:

// at the end of Package.swift
let combineImp = ProcessInfo.processInfo.combineImplementation
for target in package.targets {
    target.swiftSettings.append(contentsOf: combineImp.swiftSettings)
}
#if USE_COMBINE
// do something when use Apple Combine
#elseif USE_COMBINEX
// do something else when use CombineX
#endif