Skip to content

Commit

Permalink
Merge pull request #88 from NordicSemiconductor/feature/preview-perip…
Browse files Browse the repository at this point in the history
…heral

Simple CBMPeripheral implementation for Swift UI Previews
  • Loading branch information
philips77 authored Mar 3, 2023
2 parents e257e80 + 4d3e89a commit a0eac68
Show file tree
Hide file tree
Showing 8 changed files with 2,070 additions and 1,866 deletions.
30 changes: 26 additions & 4 deletions CoreBluetoothMock/CBMAttributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,24 @@ open class CBMService: CBMAttribute {
self.isPrimary = isPrimary
}

internal init(shallowCopy service: CBMService,
for peripheral: CBMPeripheralMock) {
init(shallowCopy service: CBMService,
for peripheral: CBMPeripheral) {
self.identifier = service.identifier
self.peripheral = peripheral
self._uuid = service._uuid
self.isPrimary = service.isPrimary
}

convenience init(copy service: CBMService,
for peripheral: CBMPeripheral) {
self.init(shallowCopy: service, for: peripheral)
self._includedServices = service._includedServices?.map { includedService in
CBMService(copy: includedService, for: peripheral)
}
self._characteristics = service._characteristics?.map { characteristic in
CBMCharacteristic(copy: characteristic, in: self)
}
}
}

internal class CBMServiceNative: CBMService {
Expand Down Expand Up @@ -139,8 +150,8 @@ open class CBMServiceMock: CBMService {
/// - includedServices: Optional list of included services.
/// - characteristics: Optional list of characteristics.
public convenience init(type uuid: CBMUUID, primary isPrimary: Bool,
includedService: CBMServiceMock...,
characteristics: CBMCharacteristicMock...) {
includedService: CBMServiceMock...,
characteristics: CBMCharacteristicMock...) {
self.init(type: uuid,
primary: isPrimary,
includedService: includedService,
Expand Down Expand Up @@ -237,6 +248,13 @@ open class CBMCharacteristic: CBMAttribute {
self.properties = characteristic.properties
self.isNotifying = false
}

convenience init(copy characteristic: CBMCharacteristic, in service: CBMService) {
self.init(shallowCopy: characteristic, in: service)
self._descriptors = characteristic._descriptors?.map { descriptor in
CBMDescriptor(copy: descriptor, in: self)
}
}
}

internal class CBMCharacteristicNative: CBMCharacteristic {
Expand Down Expand Up @@ -343,6 +361,10 @@ open class CBMDescriptor: CBMAttribute {
self.characteristic = characteristic
self._uuid = descriptor._uuid
}

convenience init(copy descriptor: CBMDescriptor, in characteristic: CBMCharacteristic) {
self.init(shallowCopy: descriptor, in: characteristic)
}
}

internal class CBMDescriptorNative: CBMDescriptor {
Expand Down
38 changes: 38 additions & 0 deletions CoreBluetoothMock/CBMCentralManagerMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ open class CBMCentralManagerMock: CBMCentralManager {
initializeAdvertising()
}
}
/// A list of ``CBMPeripheral``s for SwiftUI Previews only.
///
/// Registered items can be accessed using any ``CBMCentralManagerMock``.
private static var previewPeripherals: Set<CBMPeripheralPreview> = Set()
/// A map of all current advertisements of all simulated peripherals.
private static var advertisementTimers: [CBMAdvertisementConfig : Timer] = [:]
/// A mutex queue for managing managers.
Expand Down Expand Up @@ -348,6 +352,14 @@ open class CBMCentralManagerMock: CBMCentralManager {

// MARK: - Central manager simulation methods

/// This method may be used to register a list ot ``CBMPeripheralPreview`` should they be used in Swift UI Previews.
///
/// Registered peripherals can be connected, retrieved, and respond to basic requests
/// - Parameter peripherals: The list of peripherals intended for Swift UI purposes.
internal static func registerForPreviews(_ peripheral: CBMPeripheralPreview) {
previewPeripherals.insert(peripheral)
}

/// Removes all active central manager instances and peripherals from the
/// simulation, resetting it to the initial state.
///
Expand Down Expand Up @@ -688,6 +700,12 @@ open class CBMCentralManagerMock: CBMCentralManager {
}

open override func connect(_ peripheral: CBMPeripheral, options: [String : Any]? = nil) {
// Handle the Preview peripheral.
if let peripheral = peripheral as? CBMPeripheralPreview {
peripheral.state = .connected
delegate?.centralManager(self, didConnect: peripheral)
return
}
// Central manager must be in powered on state.
guard ensurePoweredOn() else { return }
if let o = options, !o.isEmpty {
Expand All @@ -709,6 +727,12 @@ open class CBMCentralManagerMock: CBMCentralManager {
}

open override func cancelPeripheralConnection(_ peripheral: CBMPeripheral) {
// Handle the Preview peripheral.
if let peripheral = peripheral as? CBMPeripheralPreview {
peripheral.state = .disconnected
delegate?.centralManager(self, didDisconnectPeripheral: peripheral, error: nil)
return
}
// Central manager must be in powered on state.
guard ensurePoweredOn() else { return }
// Ignore peripherals that are not mocks.
Expand All @@ -727,6 +751,12 @@ open class CBMCentralManagerMock: CBMCentralManager {
}

open override func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBMPeripheral] {
// Check if any Preview peripheral matches the identifier.
let previewPeripherals = Self.previewPeripherals
.filter{ identifiers.contains($0.identifier) }
if !previewPeripherals.isEmpty {
return Array(previewPeripherals)
}
// Starting from iOS 13, this method returns peripherals only in ON state.
guard ensurePoweredOn() else { return [] }
// Also, look for them among other managers, and copy them to the local
Expand Down Expand Up @@ -768,6 +798,14 @@ open class CBMCentralManagerMock: CBMCentralManager {
}

open override func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBMUUID]) -> [CBMPeripheral] {
// Check if there exist any Preview peripheral with at least one common service.
let previewPeripherals = Self.previewPeripherals
.filter { peripheral in
peripheral.services?.contains(where: { serviceUUIDs.contains($0.uuid) }) ?? false
}
if !previewPeripherals.isEmpty {
return Array(previewPeripherals)
}
// Starting from iOS 13, this method returns peripherals only in ON state.
guard ensurePoweredOn() else { return [] }
// Get the connected peripherals with at least one of the given services
Expand Down
131 changes: 131 additions & 0 deletions CoreBluetoothMock/CBMPeripheralPreview.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright (c) 2020, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

import Foundation

/// A stub ``CBMPeripheral`` implementation designed only for SwiftUI Previews.
///
/// The `CBMPeripheralPreview` object has very limited functionality. The implementation
/// handles connection but all request methods just call corresponding delegate method.
///
/// All ``CBMService``s are available immediately, without the need for service discovery.
/// Bluetooth LE operations are NO OP. The device does not need to be scanned to
/// be retrievable by any ``CBMCentralManagerMock`` instance.
open class CBMPeripheralPreview: CBMPeripheral {
private let mock: CBMPeripheralSpec

public let identifier: UUID
open var name: String? {
mock.name
}
public var services: [CBMService]?

public var delegate: CBMPeripheralDelegate?
public internal(set) var state: CBMPeripheralState

public let canSendWriteWithoutResponse: Bool = true
public let ancsAuthorized: Bool = false

/// Creates the preview ``CBMPeripheral``.
/// - Parameters:
/// - mock: The mock peripheral to base the preview on.
/// - state: The state to return from ``CBMPeripheral/state``.
public init(_ mock: CBMPeripheralSpec,
state: CBMPeripheralState = .disconnected) {
self.mock = mock
self.identifier = mock.identifier
self.state = state
self.services = mock.services?.map { CBMService(copy: $0, for: self) }
CBMCentralManagerMock.registerForPreviews(self)
}

open func readRSSI() {
delegate?.peripheral(self, didReadRSSI: mock.proximity.RSSI as NSNumber, error: nil)
}

open func discoverServices(_ serviceUUIDs: [CBMUUID]?) {
delegate?.peripheral(self, didDiscoverServices: nil)
}

open func discoverIncludedServices(_ includedServiceUUIDs: [CBMUUID]?, for service: CBMService) {
delegate?.peripheral(self, didDiscoverIncludedServicesFor: service, error: nil)
}

open func discoverCharacteristics(_ characteristicUUIDs: [CBMUUID]?, for service: CBMService) {
delegate?.peripheral(self, didDiscoverCharacteristicsFor: service, error: nil)
}

open func discoverDescriptors(for characteristic: CBMCharacteristic) {
delegate?.peripheral(self, didDiscoverDescriptorsFor: characteristic, error: nil)
}

open func readValue(for characteristic: CBMCharacteristic) {
delegate?.peripheral(self, didUpdateValueFor: characteristic, error: nil)
}

open func readValue(for descriptor: CBMDescriptor) {
delegate?.peripheral(self, didUpdateValueFor: descriptor, error: nil)
}

open func maximumWriteValueLength(for type: CBMCharacteristicWriteType) -> Int {
return (mock.mtu ?? 23) - 3
}

open func writeValue(_ data: Data, for characteristic: CBMCharacteristic,
type: CBMCharacteristicWriteType) {
if type == .withResponse {
delegate?.peripheral(self, didWriteValueFor: characteristic, error: nil)
}
}

open func writeValue(_ data: Data, for descriptor: CBMDescriptor) {
delegate?.peripheral(self, didWriteValueFor: descriptor, error: nil)
}

open func setNotifyValue(_ enabled: Bool, for characteristic: CBMCharacteristic) {
delegate?.peripheral(self, didUpdateNotificationStateFor: characteristic, error: nil)
}

open func openL2CAPChannel(_ PSM: CBML2CAPPSM) {
fatalError("Not available")
}
}

extension CBMPeripheralPreview: Hashable {

public static func == (lhs: CBMPeripheralPreview, rhs: CBMPeripheralPreview) -> Bool {
return lhs.identifier == rhs.identifier
}

public func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}

}
1 change: 1 addition & 0 deletions CoreBluetoothMock/Documentation.docc/CoreBluetoothMock.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ Keys used to pass state restoration options to the central manager initializer.
- ``CBMPeripheralDelegateProxyWithL2CAPChannel``
- ``CBMPeripheralMock``
- ``CBMPeripheralNative``
- ``CBMPeripheralPreview``
- ``CBMPeripheralState``

### Attributes
Expand Down
2 changes: 1 addition & 1 deletion Example/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ EXTERNAL SOURCES:
:path: "../"

SPEC CHECKSUMS:
CoreBluetoothMock: 3f9cb3ca87380b5143648f856899d04a5b143415
CoreBluetoothMock: 31b500dd8a40328f552d543a527962b08e129f10

PODFILE CHECKSUM: dbe11fdd34f545de2b6358750fede7261d00aa0a

Expand Down
3 changes: 2 additions & 1 deletion Example/Pods/Local Podspecs/CoreBluetoothMock.podspec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Example/Pods/Manifest.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a0eac68

Please sign in to comment.