diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 5645dcd..1b79e23 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -15,8 +15,14 @@ jobs: runs-on: macos-latest steps: - - uses: actions/checkout@v4 - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v + - uses: actions/checkout@v4 + - name: Setup Swift 6.2 + uses: swift-actions/setup-swift@v3 + with: + swift-version: "6.2" # Specify the desired Swift version + - name: Get Swift version + run: swift --version # Verify the installed Swift version + - name: Build Swift Package + run: swift build -v + - name: Run Swift Package Tests + run: swift test -v diff --git a/Package.swift b/Package.swift index e178d98..6833cc0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/BLECombineKit/Central/BLECentralManager.swift b/Sources/BLECombineKit/Central/BLECentralManager.swift index 4c4bab2..82e89b2 100644 --- a/Sources/BLECombineKit/Central/BLECentralManager.swift +++ b/Sources/BLECombineKit/Central/BLECentralManager.swift @@ -11,7 +11,7 @@ import CoreBluetooth import Foundation /// Interface definining the Bluetooth Central Manager that provides Combine APIs. -public protocol BLECentralManager: AnyObject { +public protocol BLECentralManager: AnyObject, Sendable { /// Reference to the actual Bluetooth Manager, which is conveniently wrapped. var associatedCentralManager: CBCentralManagerWrapper { get } diff --git a/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift b/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift index 657e86a..17824c5 100644 --- a/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift +++ b/Sources/BLECombineKit/Central/BLECentralManagerDelegate.swift @@ -11,7 +11,7 @@ typealias DidDiscoverAdvertisementDataResult = ( peripheral: CBPeripheralWrapper, advertisementData: [String: Any], rssi: NSNumber ) -final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate { +final class BLECentralManagerDelegate: NSObject, CBCentralManagerDelegate, @unchecked Sendable { let didConnectPeripheral = PassthroughSubject() let didDisconnectPeripheral = PassthroughSubject() diff --git a/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift b/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift index 68e8aea..b4c3cd5 100644 --- a/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift +++ b/Sources/BLECombineKit/Central/CBCentralManagerWrapper.swift @@ -11,7 +11,7 @@ import Foundation /// Interface for wrapping the CBCentralManager. /// This interface is critical in order to mock the CBCentralManager calls. -public protocol CBCentralManagerWrapper { +public protocol CBCentralManagerWrapper: Sendable { /// The CBCentralManager this interface wraps to. /// Note that CBCentralManager conforms to CBCentralManagerWrapper and this getter interface is a convenient way to avoid an expensive downcast. That is, if you need a fixed reference to the CBCentralManager object do not run `let validManager = manager as? CBCentralManager`, simply run `let validManager = manager.wrappedManager` which will run significantly faster. var wrappedManager: CBCentralManager? { get } diff --git a/Sources/BLECombineKit/Central/StandardBLECentralManager.swift b/Sources/BLECombineKit/Central/StandardBLECentralManager.swift index d1d848d..cca6fca 100644 --- a/Sources/BLECombineKit/Central/StandardBLECentralManager.swift +++ b/Sources/BLECombineKit/Central/StandardBLECentralManager.swift @@ -10,7 +10,7 @@ import Combine import CoreBluetooth import Foundation -final class StandardBLECentralManager: BLECentralManager { +final class StandardBLECentralManager: BLECentralManager, @unchecked Sendable { /// The wrapped CBCentralManager. let associatedCentralManager: CBCentralManagerWrapper diff --git a/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift b/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift index 561fb40..3ab25a7 100644 --- a/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift +++ b/Sources/BLECombineKit/Characteristic/BLECharacteristic.swift @@ -7,9 +7,9 @@ // import Combine -import CoreBluetooth +@preconcurrency import CoreBluetooth -public struct BLECharacteristic { +public struct BLECharacteristic: Sendable { public let value: CBCharacteristic private let peripheral: BLEPeripheral diff --git a/Sources/BLECombineKit/Data/BLEData.swift b/Sources/BLECombineKit/Data/BLEData.swift index e2b313f..b206be1 100644 --- a/Sources/BLECombineKit/Data/BLEData.swift +++ b/Sources/BLECombineKit/Data/BLEData.swift @@ -8,7 +8,7 @@ import Foundation -public struct BLEData { +public struct BLEData: Sendable { public let value: Data public init(value: Data) { diff --git a/Sources/BLECombineKit/Error/BLEError.swift b/Sources/BLECombineKit/Error/BLEError.swift index 6973ea1..c3b34d0 100644 --- a/Sources/BLECombineKit/Error/BLEError.swift +++ b/Sources/BLECombineKit/Error/BLEError.swift @@ -6,18 +6,18 @@ // Copyright © 2020 Henry Serrano. All rights reserved. // -import CoreBluetooth +@preconcurrency import CoreBluetooth import Foundation -extension NSError: Identifiable { +extension NSError: @retroactive Identifiable { } -extension CBError: Hashable, Identifiable { +extension CBError: @retroactive Hashable, @retroactive Identifiable { public var id: Self { self } } -extension CBATTError: Hashable, Identifiable { +extension CBATTError: @retroactive Hashable, @retroactive Identifiable { public var id: Self { self } } diff --git a/Sources/BLECombineKit/Peripheral Manager/BLEATTRequest.swift b/Sources/BLECombineKit/Peripheral Manager/BLEATTRequest.swift index 95a7810..7578dfd 100644 --- a/Sources/BLECombineKit/Peripheral Manager/BLEATTRequest.swift +++ b/Sources/BLECombineKit/Peripheral Manager/BLEATTRequest.swift @@ -8,7 +8,7 @@ import CoreBluetooth -public protocol BLEATTRequest { +public protocol BLEATTRequest: Sendable { /// Reference to the actual request. Use this getter to obtain the CBATTRequest if needed. Note that CBATTRequest conforms to BLEATTRequest. var associatedRequest: CBATTRequest? { get } diff --git a/Sources/BLECombineKit/Peripheral Manager/BLECentral.swift b/Sources/BLECombineKit/Peripheral Manager/BLECentral.swift index 266183b..f7dc7ee 100644 --- a/Sources/BLECombineKit/Peripheral Manager/BLECentral.swift +++ b/Sources/BLECombineKit/Peripheral Manager/BLECentral.swift @@ -8,7 +8,7 @@ import CoreBluetooth -public protocol BLECentral { +public protocol BLECentral: Sendable { /// Reference to the actual central. Use this getter to obtain the CBCentral if needed. Note that CBCentral conforms to BLECentral. var associatedCentral: CBCentral? { get } diff --git a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift index ba01b1c..c5c93b4 100644 --- a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift +++ b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManager.swift @@ -24,7 +24,7 @@ import CoreBluetooth /// ``` /// cancellable.cancel() /// ``` -public protocol BLEPeripheralManager { +public protocol BLEPeripheralManager: Sendable { var state: CBManagerState { get } func observeState() -> AnyPublisher diff --git a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegate.swift b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegate.swift index bf34e3d..e46c7bf 100644 --- a/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegate.swift +++ b/Sources/BLECombineKit/Peripheral Manager/BLEPeripheralManagerDelegate.swift @@ -14,7 +14,7 @@ import Foundation /// /// See original source on [GitHub](https://github.com/Polidea/RxBluetoothKit/blob/2a95bce60fb569df57d7bec41d215fe58f56e1d4/Source/CBPeripheralManagerDelegateWrapper.swift). /// -final class BLEPeripheralManagerDelegate: NSObject, CBPeripheralManagerDelegate { +final class BLEPeripheralManagerDelegate: NSObject, CBPeripheralManagerDelegate, @unchecked Sendable { let didUpdateState = PassthroughSubject() let isReady = PassthroughSubject() diff --git a/Sources/BLECombineKit/Peripheral Manager/StandardBLEPeripheralManager.swift b/Sources/BLECombineKit/Peripheral Manager/StandardBLEPeripheralManager.swift index f83d94f..8660c60 100644 --- a/Sources/BLECombineKit/Peripheral Manager/StandardBLEPeripheralManager.swift +++ b/Sources/BLECombineKit/Peripheral Manager/StandardBLEPeripheralManager.swift @@ -9,7 +9,7 @@ import Combine import CoreBluetooth -final class StandardBLEPeripheralManager: BLEPeripheralManager { +final class StandardBLEPeripheralManager: BLEPeripheralManager, @unchecked Sendable { /// Implementation of the CBPeripheralManager. public let manager: CBPeripheralManager diff --git a/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift b/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift index 0b7b0cc..4ef2b44 100644 --- a/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift +++ b/Sources/BLECombineKit/Peripheral/BLEPeripheral.swift @@ -11,7 +11,7 @@ import CoreBluetooth import Foundation /// Interface definining the Bluetooth Peripheral that provides Combine APIs. -public protocol BLEPeripheral { +public protocol BLEPeripheral: Sendable { /// Reference to the actual Bluetooth peripheral, via a wrapper. var associatedPeripheral: CBPeripheralWrapper { get } diff --git a/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift b/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift index f0d93dd..f48322e 100644 --- a/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift +++ b/Sources/BLECombineKit/Peripheral/BLEPeripheralDelegate.swift @@ -29,7 +29,7 @@ typealias DidWriteValueForCharacteristicResult = ( peripheral: CBPeripheralWrapper, characteristic: CBCharacteristic, error: BLEError? ) -final class BLEPeripheralDelegate: NSObject { +final class BLEPeripheralDelegate: NSObject, @unchecked Sendable { /// Subject for the name update callback. let didUpdateName = PassthroughSubject() diff --git a/Sources/BLECombineKit/Peripheral/BLEPeripheralProvider.swift b/Sources/BLECombineKit/Peripheral/BLEPeripheralProvider.swift index af55b05..bc7668f 100644 --- a/Sources/BLECombineKit/Peripheral/BLEPeripheralProvider.swift +++ b/Sources/BLECombineKit/Peripheral/BLEPeripheralProvider.swift @@ -8,22 +8,22 @@ import Foundation -protocol BLEPeripheralProvider { +protocol BLEPeripheralProvider: Sendable { func provide( for peripheralWrapper: CBPeripheralWrapper ) -> BLETrackedPeripheral } -final class StandardBLEPeripheralProvider: BLEPeripheralProvider { +final class StandardBLEPeripheralProvider: BLEPeripheralProvider, @unchecked Sendable { private lazy var queue = DispatchQueue( label: String(describing: StandardBLEPeripheralProvider.self), attributes: .concurrent ) - private lazy var peripherals = [UUID: StandardBLEPeripheral]() + private var peripherals = [UUID: StandardBLEPeripheral]() - private weak var centralManager: BLECentralManager? + weak var centralManager: BLECentralManager? init(centralManager: BLECentralManager?) { self.centralManager = centralManager @@ -55,9 +55,15 @@ final class StandardBLEPeripheralProvider: BLEPeripheralProvider { centralManager: centralManager, delegate: peripheralDelegate ) - queue.async(flags: .barrier) { [weak self] in - self?.peripherals[peripheralWrapper.identifier] = blePeripheral + let uncheckedPeripheralWrapper = UncheckedSendable(peripheralWrapper) + queue.sync(flags: .barrier) { + self.peripherals[uncheckedPeripheralWrapper.value.identifier] = blePeripheral } return blePeripheral } } + +private struct UncheckedSendable: @unchecked Sendable { + let value: T + init(_ value: T) { self.value = value } +} diff --git a/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift b/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift index 26c57e2..d8b24d7 100644 --- a/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift +++ b/Sources/BLECombineKit/Peripheral/CBPeripheralWrapper.swift @@ -9,7 +9,7 @@ import CoreBluetooth import Foundation -public protocol CBPeripheralWrapper { +public protocol CBPeripheralWrapper: Sendable { /// The CBCentralManager this interface wraps to. /// Note that CBPeripheral conforms to CBPeripheralWrapper and this getter interface is a convenient way to avoid an expensive downcast. That is, if you need a fixed reference to the CBPeripheral object do not run `let validPeripheral = peripheral as? CBPeripheral`, simply run `let validPeripheral = peripheral.wrappedPeripheral` which will run significantly faster. var wrappedPeripheral: CBPeripheral? { get } diff --git a/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift b/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift index a4add68..725f0a3 100644 --- a/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift +++ b/Sources/BLECombineKit/Peripheral/StandardBLEPeripheral.swift @@ -9,7 +9,7 @@ import Combine import CoreBluetooth -final class StandardBLEPeripheral: BLETrackedPeripheral { +final class StandardBLEPeripheral: BLETrackedPeripheral, @unchecked Sendable { /// Subject used for tracking the lateset connection state. let connectionState = CurrentValueSubject(false) @@ -20,7 +20,10 @@ final class StandardBLEPeripheral: BLETrackedPeripheral { /// Reference to the wrapper delegate used for tracking BLE events. private let delegate: BLEPeripheralDelegate + // Following SE-0481, this will need to become `weak let` to conform with strict concurrency. + // See https://github.com/swiftlang/swift-evolution/blob/main/proposals/0481-weak-let.md /// Reference to te BLECentralManager. + // nonisolated(unsafe) private weak var centralManager: BLECentralManager? /// Cancellable reference to the connect publisher. diff --git a/Sources/BLECombineKit/Service/BLEService.swift b/Sources/BLECombineKit/Service/BLEService.swift index c5954a9..3743421 100644 --- a/Sources/BLECombineKit/Service/BLEService.swift +++ b/Sources/BLECombineKit/Service/BLEService.swift @@ -7,10 +7,10 @@ // import Combine -import CoreBluetooth +@preconcurrency import CoreBluetooth import Foundation -public struct BLEService { +public struct BLEService: Sendable { public let value: CBService private let peripheral: BLEPeripheral diff --git a/Sources/BLECombineKit/Utils/Publisher+AsyncStream.swift b/Sources/BLECombineKit/Utils/Publisher+AsyncStream.swift index 8958ef0..6eca450 100644 --- a/Sources/BLECombineKit/Utils/Publisher+AsyncStream.swift +++ b/Sources/BLECombineKit/Utils/Publisher+AsyncStream.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Henry Serrano. All rights reserved. // -import Combine +@preconcurrency import Combine -extension AnyPublisher { +extension AnyPublisher where Output: Sendable { var asyncThrowingStream: AsyncThrowingStream { return AsyncThrowingStream { continuation in let cancellable = @@ -23,9 +23,16 @@ extension AnyPublisher { } receiveValue: { data in continuation.yield(data) } + + let uncheckedCancellable = UncheckedSendable(cancellable) continuation.onTermination = { _ in - cancellable.cancel() + uncheckedCancellable.value.cancel() } } } } + +private struct UncheckedSendable: @unchecked Sendable { + let value: T + init(_ value: T) { self.value = value } +} diff --git a/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift b/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift index 90a7936..f7a77b1 100644 --- a/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift +++ b/Tests/BLECombineKitTests/Mocks/BLEPeripheralMocks.swift @@ -6,18 +6,18 @@ // Copyright © 2020 Henry Serrano. All rights reserved. // -import Combine -import CoreBluetooth +@preconcurrency import Combine +@preconcurrency import CoreBluetooth import Foundation @testable import BLECombineKit -struct SetNotifyValueWasCalledStackValue: Equatable { +struct SetNotifyValueWasCalledStackValue: Equatable, @unchecked Sendable { let enabled: Bool let characteristic: CBCharacteristic } -final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { +final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral, @unchecked Sendable { let connectionState = CurrentValueSubject(false) var associatedPeripheral: CBPeripheralWrapper @@ -134,7 +134,7 @@ final class MockBLEPeripheral: BLEPeripheral, BLETrackedPeripheral { } -final class MockCBPeripheralWrapper: CBPeripheralWrapper { +final class MockCBPeripheralWrapper: CBPeripheralWrapper, @unchecked Sendable { var wrappedPeripheral: CBPeripheral? var state = CBPeripheralState.connected diff --git a/Tests/BLECombineKitTests/Mocks/MockBLECentralManager.swift b/Tests/BLECombineKitTests/Mocks/MockBLECentralManager.swift index 300242b..bba0601 100644 --- a/Tests/BLECombineKitTests/Mocks/MockBLECentralManager.swift +++ b/Tests/BLECombineKitTests/Mocks/MockBLECentralManager.swift @@ -6,13 +6,13 @@ // Copyright © 2020 Henry Serrano. All rights reserved. // -import Combine -import CoreBluetooth +@preconcurrency import Combine +@preconcurrency import CoreBluetooth import Foundation @testable import BLECombineKit -final class MockBLECentralManager: BLECentralManager { +final class MockBLECentralManager: BLECentralManager, @unchecked Sendable { private var _state = CurrentValueSubject(CBManagerState.unknown) var state: AnyPublisher { diff --git a/Tests/BLECombineKitTests/Mocks/MockBLEPeripheralProvider.swift b/Tests/BLECombineKitTests/Mocks/MockBLEPeripheralProvider.swift index 8aa8aec..6e9275b 100644 --- a/Tests/BLECombineKitTests/Mocks/MockBLEPeripheralProvider.swift +++ b/Tests/BLECombineKitTests/Mocks/MockBLEPeripheralProvider.swift @@ -10,7 +10,7 @@ import Foundation @testable import BLECombineKit -final class MockBLEPeripheralProvider: BLEPeripheralProvider { +final class MockBLEPeripheralProvider: BLEPeripheralProvider, @unchecked Sendable { var buildBLEPeripheralWasCalledCount = 0 var blePeripheral: BLETrackedPeripheral? @@ -23,7 +23,7 @@ final class MockBLEPeripheralProvider: BLEPeripheralProvider { } /// Internal only: Used for returning nil peripheral on multiple build calls -final class MockArrayBLEPeripheralBuilder: BLEPeripheralProvider { +final class MockArrayBLEPeripheralBuilder: BLEPeripheralProvider, @unchecked Sendable { var buildBLEPeripheralWasCalledCount = 0 var blePeripherals = [BLETrackedPeripheral]() diff --git a/Tests/BLECombineKitTests/Mocks/MockCBCentralManagerWrapper.swift b/Tests/BLECombineKitTests/Mocks/MockCBCentralManagerWrapper.swift index ff85795..be48f6a 100644 --- a/Tests/BLECombineKitTests/Mocks/MockCBCentralManagerWrapper.swift +++ b/Tests/BLECombineKitTests/Mocks/MockCBCentralManagerWrapper.swift @@ -6,12 +6,12 @@ // Copyright © 2021 Henry Serrano. All rights reserved. // -import CoreBluetooth +@preconcurrency import CoreBluetooth import Foundation @testable import BLECombineKit -final class MockCBCentralManagerWrapper: CBCentralManagerWrapper { +final class MockCBCentralManagerWrapper: CBCentralManagerWrapper, @unchecked Sendable { var wrappedManager: CBCentralManager? var isScanning: Bool = false diff --git a/Tests/BLECombineKitTests/Mocks/MockPeripheralManager.swift b/Tests/BLECombineKitTests/Mocks/MockPeripheralManager.swift index 805c711..b8f0309 100644 --- a/Tests/BLECombineKitTests/Mocks/MockPeripheralManager.swift +++ b/Tests/BLECombineKitTests/Mocks/MockPeripheralManager.swift @@ -7,9 +7,10 @@ // import BLECombineKit -import CoreBluetooth +@preconcurrency import CoreBluetooth +import Foundation -final class MockCBPeripheralManager: CBPeripheralManager { +final class MockCBPeripheralManager: CBPeripheralManager, @unchecked Sendable { struct UpdateValueStackValue: Equatable { let value: Data let characteristic: CBMutableCharacteristic @@ -53,7 +54,7 @@ final class MockCBPeripheralManager: CBPeripheralManager { } } -final class MockBLECentral: BLECentral { +final class MockBLECentral: BLECentral, @unchecked Sendable { var associatedCentral: CBCentral? var identifier = UUID() @@ -61,7 +62,7 @@ final class MockBLECentral: BLECentral { var maximumUpdateValueLength: Int = 0 } -final class MockBLEATTRequest: BLEATTRequest { +final class MockBLEATTRequest: BLEATTRequest, @unchecked Sendable { var associatedRequest: CBATTRequest? var centralWrapper: BLECentral = MockBLECentral()