Skip to content

Commit

Permalink
Merge pull request #42 from netguru/Add-handler-for-connection-cancel…
Browse files Browse the repository at this point in the history
…led-manually

Add handler for peripheral connection cancelled
  • Loading branch information
filip-zielinski authored Apr 11, 2022
2 parents 6175293 + 2d5a818 commit 3d8e5d5
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
xcuserdata
profile
build
.build
output
DerivedData
*.mode1v3
Expand Down
2 changes: 1 addition & 1 deletion BlueSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |spec|

spec.name = 'BlueSwift'
spec.version = '1.0.5'
spec.version = '1.0.6'
spec.summary = 'Easy and lightweight CoreBluetooth wrapper written in Swift'
spec.homepage = 'https://github.com/netguru/BlueSwift'

Expand Down
8 changes: 8 additions & 0 deletions Bluetooth.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
38FE6BE7200689AB00809A06 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE6BA82006898100809A06 /* ConfigurationTests.swift */; };
3A369634210F1C4E007C62F1 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A369633210F1C4E007C62F1 /* CoreBluetooth.framework */; };
3A369636210F1C57007C62F1 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A369635210F1C57007C62F1 /* XCTest.framework */; };
4BEB6E6F2800735D0061C702 /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEB6E6E2800735D0061C702 /* Sequence.swift */; };
4BEB6E72280077010061C702 /* SequenceExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEB6E70280075510061C702 /* SequenceExtensionTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -150,6 +152,8 @@
3A142D24210F0EEE000A6089 /* Sample-Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Sample-Base.xcconfig"; sourceTree = "<group>"; };
3A369633210F1C4E007C62F1 /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.4.sdk/System/Library/Frameworks/CoreBluetooth.framework; sourceTree = DEVELOPER_DIR; };
3A369635210F1C57007C62F1 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
4BEB6E6E2800735D0061C702 /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = "<group>"; };
4BEB6E70280075510061C702 /* SequenceExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceExtensionTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -306,6 +310,7 @@
isa = PBXGroup;
children = (
38FE6BA72006898100809A06 /* ExtensionTests.swift */,
4BEB6E70280075510061C702 /* SequenceExtensionTests.swift */,
382CD41E2028F4B50065DFF6 /* CommandTests.swift */,
38FE6BA82006898100809A06 /* ConfigurationTests.swift */,
38FE6BA92006898100809A06 /* Info.plist */,
Expand Down Expand Up @@ -387,6 +392,7 @@
38FE6BBB2006898100809A06 /* CBUUID.swift */,
38FE6BBC2006898100809A06 /* String.swift */,
385B01AC2007C93000E3D478 /* Array.swift */,
4BEB6E6E2800735D0061C702 /* Sequence.swift */,
3863BE73200D582600FD4EC9 /* Int.swift */,
3863BE75200D5B9B00FD4EC9 /* Data.swift */,
3863BE68200BDDBA00FD4EC9 /* CBCharacteristic.swift */,
Expand Down Expand Up @@ -620,6 +626,7 @@
files = (
38BE35DC203DD67D0018EA0D /* ConnectablePeripheral.swift in Sources */,
3863BE69200BDDBA00FD4EC9 /* CBCharacteristic.swift in Sources */,
4BEB6E6F2800735D0061C702 /* Sequence.swift in Sources */,
385B01AD2007C93000E3D478 /* Array.swift in Sources */,
38FE6BD82006898100809A06 /* String.swift in Sources */,
382CD41C2028E5950065DFF6 /* Int.swift in Sources */,
Expand All @@ -645,6 +652,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4BEB6E72280077010061C702 /* SequenceExtensionTests.swift in Sources */,
382CD41F2028F4B50065DFF6 /* CommandTests.swift in Sources */,
38FE6BE7200689AB00809A06 /* ConfigurationTests.swift in Sources */,
38FE6BE6200689A700809A06 /* ExtensionTests.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Bluetooth/ConnectionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import UIKit
import BlueSwift

class ConnectionViewController: UIViewController {

@IBOutlet weak var notifyLabel: UILabel!
@IBOutlet weak var readLabel: UILabel!
@IBOutlet weak var textField: UITextField!
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Change Log
All notable changes to this project will be documented in this file.

## [1.0.6] - 2022-04-08

### Added

- added public `peripheralConnectionCancelledHandler(_:)` setable property to `BluetoothConnection` class. It is called when disconnecting a peripheral using `disconnect(_:)` is completed

### Changed

- refactored `.filter(_:).first` to `first(where:)` for optimisation
2 changes: 1 addition & 1 deletion Configurations/Common/Common-Base.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

#include "../xcconfigs/Common/Common.xcconfig"

_BUILD_VERSION = 1.0.0
_BUILD_VERSION = 1.0.6
_BUILD_NUMBER = 1

_DEPLOYMENT_TARGET_IOS = 11.0
Expand Down
18 changes: 13 additions & 5 deletions Framework/Source Files/Connection/BluetoothConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import CoreBluetooth

/// Public facing interface granting methods to connect and disconnect devices.
public final class BluetoothConnection: NSObject {

/// A singleton instance.
public static let shared = BluetoothConnection()

/// Connection service implementing native CoreBluetooth stack.
private var connectionService = ConnectionService()

/// A advertisement validation handler. Will be called upon every peripheral discovery. Return value from this closure
/// will indicate if manager should or shouldn't start connection with the passed peripheral according to it's identifier
/// and advertising packet.
Expand All @@ -34,7 +34,15 @@ public final class BluetoothConnection: NSObject {
connectionService.peripheralValidationHandler = peripheralValidationHandler
}
}


/// A peripheral connection cancelled handler. Called when disconnecting a peripheral using `disconnect(_:)` is completed.
/// Contains matched peripheral and native peripheral from CoreBluetooth.
public var peripheralConnectionCancelledHandler: ((Peripheral<Connectable>, CBPeripheral) -> Void)? {
didSet {
connectionService.peripheralConnectionCancelledHandler = peripheralConnectionCancelledHandler
}
}

/// Primary method used to connect to a device. Can be called multiple times to connect more than on device at the same time.
///
/// - Parameters:
Expand All @@ -56,7 +64,7 @@ public final class BluetoothConnection: NSObject {
handler?(error)
}
}

/// Primary method to disconnect a device. If it's not yet connected it'll be removed from connection queue, and connection attempts will stop.
///
/// - Parameter peripheral: a peripheral you wish to disconnect. Should be exactly the same instance that was used for connection.
Expand Down
54 changes: 34 additions & 20 deletions Framework/Source Files/Connection/ConnectionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ internal final class ConnectionService: NSObject {

/// Closure used to manage connection success or failure.
internal var connectionHandler: ((Peripheral<Connectable>, ConnectionError?) -> ())?


/// Closure called when disconnecting a peripheral using `disconnect(_:)` is completed.
internal var peripheralConnectionCancelledHandler: ((Peripheral<Connectable>, CBPeripheral) -> ())?

/// Returns the amount of devices already connected.
internal var connectedDevicesAmount: Int {
return peripherals.filter { $0.isConnected }.count
Expand All @@ -33,7 +36,10 @@ internal final class ConnectionService: NSObject {

/// Set of peripherals the manager should connect.
private var peripherals = [Peripheral<Connectable>]()


/// Handle to peripherals which were requested to disconnect.
private var peripheralsToDisconnect = [Peripheral<Connectable>]()

private weak var connectingPeripheral: Peripheral<Connectable>?

/// Connection options - means you will be notified on connection and disconnection of devices.
Expand Down Expand Up @@ -76,7 +82,8 @@ extension ConnectionService {
/// Disconnects given device.
internal func disconnect(_ peripheral: CBPeripheral) {
if let index = peripherals.firstIndex(where: { $0.peripheral === peripheral }) {
peripherals.remove(at: index)
let peripheralToDisconnect = peripherals.remove(at: index)
peripheralsToDisconnect.append(peripheralToDisconnect)
}
centralManager.cancelPeripheralConnection(peripheral)
}
Expand Down Expand Up @@ -152,9 +159,9 @@ extension ConnectionService: CBCentralManagerDelegate {
let devices = peripherals.filter({ $0.configuration.matches(advertisement: advertisementData)})

guard let handler = peripheralValidationHandler,
let matchingPeripheral = devices.filter({ $0.peripheral == nil }).first,
handler(matchingPeripheral, peripheral, advertisementData, RSSI),
connectingPeripheral == nil
let matchingPeripheral = devices.first(where: { $0.peripheral == nil }),
handler(matchingPeripheral, peripheral, advertisementData, RSSI),
connectingPeripheral == nil
else {
return
}
Expand All @@ -170,7 +177,7 @@ extension ConnectionService: CBCentralManagerDelegate {
/// Called upon a successfull peripheral connection.
/// - SeeAlso: CBCentralManagerDelegate
public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
guard let connectingPeripheral = peripherals.filter({ $0.peripheral === peripheral }).first else { return }
guard let connectingPeripheral = peripherals.first(withIdentical: peripheral) else { return }
self.connectingPeripheral = connectingPeripheral
connectingPeripheral.peripheral = peripheral
peripheral.delegate = self
Expand All @@ -185,7 +192,7 @@ extension ConnectionService: CBCentralManagerDelegate {
}

extension ConnectionService: CBPeripheralDelegate {

/// Called upon discovery of services of a connected peripheral. Used to map model services to passed configuration and
/// discover characteristics for each matching service.
/// - SeeAlso: CBPeripheralDelegate
Expand All @@ -200,13 +207,13 @@ extension ConnectionService: CBPeripheralDelegate {
peripheral.discoverCharacteristics(service.characteristics.map({ $0.bluetoothUUID }), for: cbService)
})
}

/// Called upon discovery of characteristics of a connected peripheral per each passed service. Used to map CBCharacteristic
/// instances to passed configuration, assign characteristic raw values and setup notifications.
/// - SeeAlso: CBPeripheralDelegate
public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard let characteristics = service.characteristics, error == nil else { return }
let matchingService = connectingPeripheral?.configuration.services.filter({ $0.bluetoothUUID == service.uuid }).first
let matchingService = connectingPeripheral?.configuration.services.first(where: { $0.bluetoothUUID == service.uuid })
let matchingCharacteristics = matchingService?.characteristics.matchingElementsWith(characteristics)
guard matchingCharacteristics?.count != 0 else {
centralManager.cancelPeripheralConnection(peripheral)
Expand All @@ -223,19 +230,26 @@ extension ConnectionService: CBPeripheralDelegate {
}
connectingPeripheral = nil
}

/// Called when device is disconnected, inside this method a device is reconnected. Connect method does not have a timeout
/// so connection will be triggered anytime in the future when the device is discovered. In case the connection is no
/// longer needed we'll just return.

/// Called when device is disconnected.
/// If connection was cancelled using `disconnect(_:)`, then `peripheralConnectionCancelledHandler(_:)` is called.
/// Otherwise device is reconnected. Connect method does not have a timeout, so connection will be triggered
/// anytime in the future when the device is discovered. In case the connection is no longer needed we'll just return.
/// - SeeAlso: CBPeripheralDelegate
public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
guard
let disconnectedPeripheral = peripherals.filter({ $0.peripheral === peripheral }).first,
let nativePeripheral = disconnectedPeripheral.peripheral
else {
/// `error` is nil if disconnect resulted from a call to `cancelPeripheralConnection(_:)`.
/// SeeAlso: https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518791-centralmanager
if error == nil,
let disconnectedPeripheral = peripheralsToDisconnect.first(withIdentical: peripheral) {
peripheralsToDisconnect.removeAll(where: { $0 === disconnectedPeripheral })
peripheralConnectionCancelledHandler?(disconnectedPeripheral, peripheral)
return
}
disconnectedPeripheral.disconnectionHandler?()
centralManager.connect(nativePeripheral, options: connectionOptions)

if let disconnectedPeripheral = peripherals.first(withIdentical: peripheral),
let nativePeripheral = disconnectedPeripheral.peripheral {
disconnectedPeripheral.disconnectionHandler?()
centralManager.connect(nativePeripheral, options: connectionOptions)
}
}
}
41 changes: 41 additions & 0 deletions Framework/Source Files/Extensions/Sequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Sequence.swift
// Bluetooth
//
// Created by Filip Zieliński on 08/04/2022.
// Copyright © 2022 Netguru. All rights reserved.
//

import Foundation
import CoreBluetooth

internal extension Sequence {

/// Returns the first element of the sequence where given element property is identical (`===`) to given object instance.
/// - Parameters:
/// - propertyKeyPath: a key path to an element property.
/// - instance: an object instance to compare against.
/// - Returns: The first element of the sequence where given element property is identical (`===`) to given object instance, or `nil` if there is no such element.
func first<Object: AnyObject>(where propertyKeyPath: KeyPath<Element, Object>, isIdenticalTo instance: Object) -> Element? {
first { $0[keyPath: propertyKeyPath] === instance }
}

/// Returns the first element of the sequence where given element optional property is identical (`===`) to given object instance.
/// - Parameters:
/// - propertyKeyPath: a key path to an element property.
/// - instance: an object instance to compare against.
/// - Returns: The first element of the sequence where given element optional property is identical (`===`) to given object instance, or `nil` if there is no such element.
func first<Object: AnyObject>(where propertyKeyPath: KeyPath<Element, Object?>, isIdenticalTo instance: Object) -> Element? {
first { $0[keyPath: propertyKeyPath] === instance }
}
}

internal extension Sequence where Element == Peripheral<Connectable> {

/// Convenience method returning the first peripheral of the sequence with `peripheral` property identical (`===`) to given `CBPeripheral` instance.
/// - Parameter cbPeripheral: a `CBPeripheral` instance.
/// - Returns: The first peripheral of the sequence with `peripheral` property identical (`===`) to given `CBPeripheral` instance, or `nil` if there is no such element.
func first(withIdentical cbPeripheral: CBPeripheral) -> Element? {
first(where: \.peripheral, isIdenticalTo: cbPeripheral)
}
}
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ Just drop the line below to your Podfile:

`pod 'BlueSwift'`

(but probably you'd like to pin it to the nearest major release, so `pod 'BlueSwift' , '~> 1.0.0'`)
(but probably you'd like to pin it to the nearest major release, so `pod 'BlueSwift' , '~> 1.0.6'`)

### ![](https://img.shields.io/badge/carthage-compatible-green.svg)

The same as with Cocoapods, insert the line below to your Cartfile:

`github 'netguru/BlueSwift'`

, or including version - `github 'netguru/BlueSwift' ~> 1.0.0`
, or including version - `github 'netguru/BlueSwift' ~> 1.0.6`

## 📄 License

Expand Down
78 changes: 78 additions & 0 deletions Unit Tests/SequenceExtensionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// SequenceExtensionTests.swift
// Bluetooth
//
// Created by Filip Zieliński on 08/04/2022.
// Copyright © 2022 Netguru. All rights reserved.
//

import XCTest
import CoreBluetooth
@testable import BlueSwift

final class SequenceExtensionTests: XCTestCase {

private let fixtureReference = SimpleClass()
private let fixtureOptionalReference = SimpleClass()
private lazy var fixtureTestClass = TestClass(someReference: fixtureReference, someOptionalReference: fixtureOptionalReference)

func testFirstSequenceElement_withIdenticalProperty() {
// given:
var sut: [TestClass] = [
TestClass(someReference: .init(), someOptionalReference: nil),
TestClass(someReference: .init(), someOptionalReference: .init()),
fixtureTestClass,
TestClass(someReference: .init(), someOptionalReference: .init()),
]

// then:
testFindingElement_withMatchingIdenticalProperty(sut: sut)

// given
sut = [
fixtureTestClass,
TestClass(someReference: .init(), someOptionalReference: .init()),
TestClass(someReference: .init(), someOptionalReference: .init()),
TestClass(someReference: .init(), someOptionalReference: nil)
]

// then:
testFindingElement_withMatchingIdenticalProperty(sut: sut)

// given
sut = [
TestClass(someReference: .init(), someOptionalReference: .init()),
TestClass(someReference: .init(), someOptionalReference: nil),
TestClass(someReference: .init(), someOptionalReference: .init()),
fixtureTestClass
]

// then:
testFindingElement_withMatchingIdenticalProperty(sut: sut)
}
}

private extension SequenceExtensionTests {

func testFindingElement_withMatchingIdenticalProperty(sut: [TestClass]) {
XCTAssert(sut.first(where: \.someReference, isIdenticalTo: fixtureReference) === fixtureTestClass, "Should find object with matching identical (`===`) property")
XCTAssert(sut.first(where: \.someOptionalReference, isIdenticalTo: fixtureOptionalReference) === fixtureTestClass, "Should find object with matching identical (`===`) property")
XCTAssertNil(sut.first(where: \.someReference, isIdenticalTo: fixtureOptionalReference), "Should not find any object with matching identical (`===`) property")
XCTAssertNil(sut.first(where: \.someOptionalReference, isIdenticalTo: fixtureReference), "Should not find any object with matching identical (`===`) property")
XCTAssertNil(sut.first(where: \.someReference, isIdenticalTo: .init()), "Should not find any object with matching identical (`===`) property")
XCTAssertNil(sut.first(where: \.someOptionalReference, isIdenticalTo: .init()), "Should not find any object with matching identical (`===`) property")
}
}

private final class TestClass: Identifiable {

let someReference: SimpleClass
let someOptionalReference: SimpleClass?

init(someReference: SimpleClass, someOptionalReference: SimpleClass?) {
self.someReference = someReference
self.someOptionalReference = someOptionalReference
}
}

private final class SimpleClass {}
Loading

0 comments on commit 3d8e5d5

Please sign in to comment.