RobotPajamas / SwiftyTeeth

A simple, lightweight library intended to take away some of the cruft and tediousness of using CoreBluetooth
Apache License 2.0
23 stars 8 forks source link

Investigate async/await API #54

Open sureshjoshi opened 3 years ago

sureshjoshi commented 3 years ago

For call/response, async await can really clean up code

ericlewis commented 3 years ago

I am working on a more comprehensive package to enable this feature, but if you want to get started now, you can use the two extensions below, which somewhat resemble the Rx extensions API. Example and implementation follows. This work isn't complete, it probably needs to consider things like cancellations and what not, I hope to publish something eventually alongside an open source project of mine, making this an official extension as the APIs evolve with real world usage.

// Monitor Bluetooth state
for await state in manager.state() {
    // Skip execution until we reach a poweredOn event
    guard state == .poweredOn else { continue }

    // Scan for devices
    for await device in manager.scan() {
        // Wait till we find our device
        guard device.name == "MyDevice" else { continue }

        // Stop scanning once we have found our device...
        manager.stopScan()

        // ...and connect!
        await device.connect()

        // Discover services
        let services = try await device.services()

        for service in services {
            // Discover characteristics for a service
            let result = try await device.characteristics(for: service)

            for characterisic in result.characteristics {
                // Print each value for a given characteristic if it has one
                guard let value = try? await device.read(characterisic, in: service) else { return }
                print("\(device.id):\(service.uuid):\(characterisic.uuid): \(value)")
            }
        }
    }
}
import SwiftyTeeth

@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
public extension SwiftyTeeth {
    func state() -> AsyncStream<BluetoothState> {
        AsyncStream { continuation in
            stateChangedHandler = {
                continuation.yield($0)
            }
        }
    }

    func scan() -> AsyncStream<Device> {
        AsyncStream { continuation in
            scan {
                continuation.yield($0)
            }
        }
    }

    func scan(timeout: TimeInterval) async -> [Device] {
        await withCheckedContinuation { continuation in
            scan(for: timeout) {
                continuation.resume(returning: $0)
            }
        }
    }
}

@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
public extension Device {

    @discardableResult
    func connect(timeout: TimeInterval? = nil, autoReconnect: Bool = true) async -> ConnectionState {
        await withCheckedContinuation { continuation in
            connect(with: timeout, autoReconnect: autoReconnect) {
                continuation.resume(returning: $0)
            }
        }
    }

    func services(with uuids: [UUID]? = nil) async throws -> [Service] {
        try await withCheckedThrowingContinuation { continuation in
            discoverServices(with: uuids) {
                continuation.resume(with: $0)
            }
        }
    }

    func characteristics(with uuids: [UUID]? = nil, for service: Service) async throws -> DiscoveredCharacteristic {
        try await withCheckedThrowingContinuation { continuation in
            discoverCharacteristics(with: uuids, for: service) {
                continuation.resume(with: $0)
            }
        }
    }

    func read(_ characteristic: Characteristic, in service: Service) async throws -> Data {
        try await withCheckedThrowingContinuation { continuation in
            read(from: characteristic.uuid, in: service.uuid) {
                continuation.resume(with: $0)
            }
        }
    }

    func write(_ data: Data, to characteristic: Characteristic, in service: Service) async throws {
        try await withCheckedThrowingContinuation { continuation in
            write(data: data, to: characteristic.uuid, in: service.uuid) {
                continuation.resume(with: $0)
            }
        }
    }

    func subscribe(to characteristic: Characteristic, in service: Service) -> AsyncThrowingStream<Data, Error> {
        AsyncThrowingStream { continuation in
            subscribe(to: characteristic.uuid, in: service.uuid) {
                continuation.yield(with: $0)
            }
        }
    }
}
sureshjoshi commented 3 years ago

@ericlewis Legend!

Do you happen to know if Swift Package Manager yet lets us use multiple packages with multiple OS requirements? I last looked into this like 6 months ago, and it wasn't possible at the time.

I'd like to consolidate all the SwiftyTeeth extensions into a single repo - for maintainability.

ericlewis commented 3 years ago

Yeah, it should be possible I think. If not, then the availability checks can at least stop the code from being compiled while being a separate package.

sureshjoshi commented 3 years ago

That's true - my thinking was mostly in cases where there are deps installed, but in this case, you're right - it doesn't matter

sureshjoshi commented 1 year ago

Just a note for posterity, I've been using these in my apps that targeted iOS 15+, however, since async/await was back ported a bit - I've been using them in a few other apps.

Haven't landed them in the repo because of the naming conflicts