Writing Combine wrapper around 3rd party SDK

I have to use a 3rd party SDK in my app, so I thought it's an excellent opportunity also to use Combine in real life and wrap this SDK in a thin layer of Combine helpers. I was able to cover everything reasonably easy, but now I'm facing the issue that some of my send methods are never delivered. I'll share some of my code to draw a better picture of what I have.

final class Manager: NSObject {
    private let sdkManager: BleManager

    // Private Publishers
    private let newDeviceFoundPublisher = PassthroughSubject<Device, Error>()
    private let connectedToDevicePublisher = PassthroughSubject<Device, Error>()
    private let deviceDataPublisher = PassthroughSubject<DeviceData, Never>()

    init(manager: BleManager = BleManager.shareInstance()) {
        self.vvBleManager = manager
        super.init()
        self.sdkManager.delegate = self
    }

    func startScanning(deviceType: DeviceType) -> AnyPublisher<Device, Error> {
        sdkManager.startScan(deviceType)

        return newDeviceFoundPublisher
            .map { Device.init }
            .timeout(.seconds(10), scheduler: DispatchQueue.main, customError: { .scanningTimeout })
            .eraseToAnyPublisher()
    }

    func stopScanning() {
        sdkManager.stopScan()
    }

    func connect(to device: Device) -> AnyPublisher<Device, Error> {
        sdkManager.connect(device)
        return connectedToDevicePublisher.eraseToAnyPublisher()
    }

    func disconnect(device: DeviceType) {
        sdkManager.disconnect(device)
    }

    func deviceData() -> AnyPublisher<DeviceData, Never> {
        return deviceDataPublisher.eraseToAnyPublisher()
    }
}

extension VivalnkManager: BLEDelegate {
    func onDeviceFound(_ device: Device!) {
        newDeviceFoundPublisher.send(device)
    }

    func onConnected(_ device: Device!) {
        connectedToDevicePublisher.send(device)
        connectedToDevicePublisher.send(completion: .finished)
    }

    func onReceiveData(_ data: Any!) {
        if let newData = try? DeviceData(from: data!) {
            deviceDataPublisher.send(newData)
        }
    }
}

There's nothing crazy going on here and when I check logs from the SDK all methods are being executed correctly and the delegate is being triggered on new data etc. This is how I know there must be something wrong with my code. Now, what I'm trying to achieve and can't get it to work is presented the following code sample:

manager
    .startScanning(deviceType: .swiftometr)
    .filter { $0.deviceId == id }
    .handleEvents(receiveOutput: { _ in environment.manager.stopScanning() })
    .flatMap { environment.manager.connect(to: $0) }
    .sink(receiveCompletion: { _ in print("Completion") },
            receiveValue: { _ in print("Value") })
    .store(in: &cancellables)

As a result, I can only see Completion after some time being printed in my console, but I suspect this is from the timeout I have inside scanning method. I've tried many different approaches and nothing worked. Then, I thought, maybe flatMap doesn't work like this, so I've tried nesting a couple of URLSession.shared.dataTaskPublishers together and they worked just fine!


More digging and I'm in the state where I understand why this is all happening. It looks like sdkManager.connect(device) is happening so fast, that my application isn't able to subscribe, so that's the reason why I'd never receive any value. What helped my was simply wrapping this in a delayed block like this:

func connect(to device: Device) -> AnyPublisher<Device, Error> {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        sdkManager.connect(device)
    }

    return connectedToDevicePublisher.eraseToAnyPublisher()
}

Now connect is able to return AnyPublisher and allow subscribers to subscribe before the send happens. Even though it works, it feels like a hacky way of doing things and I'm not really happy with it. Looking for some better approaches.

1 Like

Have you tried to wrap the connection method into a defer:

defer {
        sdkManager.connect(device)
    }

A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.Preformatted text