Closures based APIs for CoreBluetooth

Overview

SwiftyBluetooth

Closures based APIs for CoreBluetooth.

Features

  • Replace the delegate based interface with a closure based interface for every CBCentralManager and CBPeripheral operation.
  • Notification based event for CBCentralManager state changes and state restoration.
  • Notification based event for CBPeripheral name updates, characteristic value updates and services updates.
  • Precise errors and guaranteed timeout for every Bluetooth operation.
  • Will automatically connect to a CBPeripheral and attempt to discover the required BLE services and characteristics required for a read or write operation if necessary.

Usage

The Library has 2 important class:

  • The Central class, a Singleton wrapper around CBCentralManager used to scan for peripherals with a closure callback and restore previous sessions.
  • The Peripheral class, a wrapper around CBPeripheral used to call CBPeripheral functions with closure callbacks.

Below are a couple examples of operations that might be of interest to you.

Scanning for Peripherals

Scan for peripherals by calling scanWithTimeout(...) while passing a timeout in seconds and a callback closure to receive Peripheral result callbacks as well as update on the status of your scan:

// You can pass in nil if you want to discover all Peripherals
SwiftyBluetooth.scanForPeripherals(withServiceUUIDs: nil, timeoutAfter: 15) { scanResult in
    switch scanResult {
        case .scanStarted:
            // The scan started meaning CBCentralManager scanForPeripherals(...) was called 
        case .scanResult(let peripheral, let advertisementData, let RSSI):
            // A peripheral was found, your closure may be called multiple time with a .ScanResult enum case.
            // You can save that peripheral for future use, or call some of its functions directly in this closure.
        case .scanStopped(let error):
            // The scan stopped, an error is passed if the scan stopped unexpectedly
    }
}        

Note that the callback closure can be called multiple times, but always start and finish with a callback containing a .scanStarted and .scanStopped result respectively. Your callback will be called with a .scanResult for every unique peripheral found during the scan.

Connecting to a peripheral

peripheral.connect { result in 
    switch result {
    case .success:
        break // You are now connected to the peripheral
    case .failure(let error):
        break // An error happened while connecting
    }
}

Reading from a peripheral's service's characteristic

If you already know the characteristic and service UUIDs you want to read from, once a peripheral has been found you can read from it right away like this:

peripheral.readValue(ofCharacWithUUID: "2A29", fromServiceWithUUID: "180A") { result in
    switch result {
    case .success(let data):
        break // The data was read and is returned as an NSData instance
    case .failure(let error):
        break // An error happened while attempting to read the data
    }
}

This will connect to the peripheral if necessary and ensure that the characteristic and service needed are discovered before reading from the characteristic matching characteristicUUID. If the charac/service cannot be retrieved you will receive an error specifying which charac/service could not be found.

If you have a reference to a CBCharacteristic, you can read using the characteristic directly:

peripheral.readValue(ofCharac: charac) { result in
    switch result {
    case .success(let data):
        break // The data was read and is returned as a Data instance
    case .failure(let error):
        break // An error happened while attempting to read the data
    }
}

Writing to a Peripheral's service's characteristic

If you already know the characteristic and service UUID you want to write to, once a peripheral has been found, you can write to that characteristic right away like this:

let data = String(0b1010).dataUsingEncoding(NSUTF8StringEncoding)!
peripheral.writeValue(ofCharacWithUUID: "1d5bc11d-e28c-4157-a7be-d8b742a013d8", 
                      fromServiceWithUUID: "4011e369-5981-4dae-b686-619dc656c7ba", 
                      value: data) { result in
    switch result {
    case .success:
        break // The write was succesful.
    case .failure(let error):
        break // An error happened while writting the data.
    }
}

Receiving Characteristic update notifications

Receiving characteristic value updates is done through notifications on the default NotificationCenter. All supported Peripheral notifications are part of the PeripheralEvent enum. Use this enum's raw values as the notification string when registering for notifications:

// First we prepare ourselves to receive update notifications 
let peripheral = somePeripheral

NotificationCenter.default.addObserver(forName: Peripheral.PeripheralCharacteristicValueUpdate, 
                                        object: peripheral, 
                                        queue: nil) { (notification) in
    let charac = notification.userInfo!["characteristic"] as! CBCharacteristic
    if let error = notification.userInfo?["error"] as? SBError {
        // Deal with error
    }
}

// We can then set a characteristic's notification value to true and start receiving updates to that characteristic
peripheral.setNotifyValue(toEnabled: true, forCharacWithUUID: "2A29", ofServiceWithUUID: "180A") { (isNotifying, error) in
    // If there were no errors, you will now receive Notifications when that characteristic value gets updated.
}

Discovering services

Discover services using the discoverServices(...) function:

peripheral.discoverServices(withUUIDs: nil) { result in
    switch result {
    case .success(let services):
        break // An array containing all the services requested
    case .failure(let error):
        break // A connection error or an array containing the UUIDs of the services that we're not found
    }
}

Like the CBPeripheral discoverServices(...) function, passing nil instead of an array of service UUIDs will discover all of this Peripheral's services.

Discovering characteristics

Discover characteristics using the discoverCharacteristics(...) function. If the service on which you are attempting to discover characteristics from has not been discovered, an attempt will first be made to discover that service for you:

peripheral.discoverCharacteristics(withUUIDs: nil, ofServiceWithUUID: "180A") { result in
    // The characteristics discovered or an error if something went wrong.
    switch result {
    case .success(let services):
        break // An array containing all the characs requested.
    case .failure(let error):
        break // A connection error or an array containing the UUIDs of the charac/services that we're not found.
    }
}

Like the CBPeripheral discoverCharacteristics(...) function, passing nil instead of an array of service UUIDs will discover all of this service's characteristics.

State preservation

SwiftyBluetooth is backed by a CBCentralManager singleton wrapper and does not give you direct access to the underlying CBCentralManager.

But, you can still setup the underlying CBCentralManager for state restoration by calling setSharedCentralInstanceWith(restoreIdentifier: ) and use the restoreIdentifier of your choice.

Take note that this method can only be called once and must be called before anything else in the library otherwise the Central sharedInstance will be lazily initiated the first time you access it.

As such, it is recommended to call it in your App Delegate's didFinishLaunchingWithOptions(:)

SwiftyBluetooth.setSharedCentralInstanceWith(restoreIdentifier: "MY_APP_BLUETOOTH_STATE_RESTORE_IDENTIFIER")

Register for state preservation notifications on the default NotificationCenter. Those notifications will contain an array of restored Peripheral.

NotificationCenter.default.addObserver(forName: Central.CentralManagerWillRestoreStateNotification,
                                        object: Central.sharedInstance, 
                                         queue: nil) { (notification) in
    if let restoredPeripherals = notification.userInfo?["peripherals"] as? [Peripheral] {

    }
}

Installation

CocoaPods

Add this to your Podfile:

pod 'SwiftyBluetooth', '~> 2.1.0'

Swift Package Manager

Simply add the library to your xcode project as a "Package Dependency"

Carthage

Add this to your Cartfile

github "jordanebelanger/SwiftyBluetooth"

Requirements

SwiftyBluetooth requires iOS 10.0+

License

SwiftyBluetooth is released under the MIT License.

Comments
  • State restoration support?

    State restoration support?

    Does the framework currently support state preservation and restoration? It doesn't seem to but I can't tell if I'm missing something. 😓

    Looking specifically at this documentation from Apple.

    opened by jakerockland 14
  • safe direct access to underlying CBCentral and CBPeripheral

    safe direct access to underlying CBCentral and CBPeripheral

    @tehjord Here is a safer solution to adding direct access to the underlying central and peripherals.

    By default, the framework won't support access, but if the user adds a configuration file, SwiftyBluetoothConfiguration.xcconfig to their root directory for the project with SWIFT_ACTIVE_COMPILATION_CONDITIONS= $(inherited) DIRECT_ACCESS, it will allow direct access.

    If you like this, I can add a more detailed description to the README for anyone who does want to enable direct access.

    opened by jakerockland 10
  • alternative connection to peripheral with identifier and services

    alternative connection to peripheral with identifier and services

    I was looking for a way to connect to a SwiftyBluetooth Peripheral given its UUID and service identifiers, and it didn't seem like this was currently possible. This is important for times where one is storing the UUID of the peripheral for faster reconnection at a later time. Thoughts @tehjord ?

    opened by jakerockland 9
  • Compilation under Xcode 13

    Compilation under Xcode 13

    I'm getting loads of warnings when trying to compile this in the latest Xcode 13 RC (with Swift5) - mostly all errors are related to this sort of thing : "Argument type 'CBService?' does not conform to expected type 'CBUUIDConvertible'"

    Any thoughts or suggestions? I'm having trouble working it out

    EDIT: maybe helpful : If I open the same project into my saved copy of 12.5.1, it compiles the first try

    opened by PFRene 8
  • Supporting connection with no timeout

    Supporting connection with no timeout

    Related to #13, I also realized that the library does not currently support connecting to a peripheral without a timeout, something that is sometimes necessary and is the normal expected behavior of the underlying CoreBluetooth framework (https://developer.apple.com/videos/play/wwdc2012/705/).

    I'm happy to implement this but were wondering your thoughts on how to expose this, as the current timeout variable has a default value, so making it an optional variable type seems a little odd. One option would be to have two different public methods, something along the lines of:

    public func connect(withTimeout timeout: TimeInterval?, completion: @escaping ConnectPeripheralCallback) {
        if let timeout = timeout {
            self.peripheralProxy.connect(timeout: timeout, completion)
        } else {
            self.peripheralProxy.connect(completion)
        }
    }
    
    public func connect(completion: @escaping ConnectPeripheralCallback) {
            self.peripheralProxy.connect(timeout: 10, completion)
    }
    
    opened by jakerockland 8
  • Add Swift Package Manager Support

    Add Swift Package Manager Support

    Moves source files into Sources directory at the root of the project and introduces a Package.swift manifest file for Swift Package Manager to manage the framework as a dependency.

    opened by levi 7
  • Connection timeout timer does not fire

    Connection timeout timer does not fire

    I'm unable to get a reliable connection timeout from this library. I've looked into it briefly and it seems the timer might be going out of scope?

    I've found for these timers, Grand Central Dispatch makes cleaner code (no selectors, no @objc, no weak vars). Try doing something like this instead of using the Timer class:

    // CentralProxy class-level
    
    var connectTimer: DispatchSourceTimer?
    
    // connect(peripheral:timeout:_:)
    
    ...
    
    self.connectTimer = DispatchSource.makeTimerSource()
    self.connectTimer!.setEventHandler {
        let uuid = request.peripheral.identifier
    
        self.connectRequests[uuid] = nil
    
        self.centralManager.cancelPeripheralConnection(request.peripheral)
    
        request.invokeCallbacks(error: SBError.operationTimedOut(operation: .connectPeripheral))
    }
    
    self.connectTimer!.scheduleOneshot(deadline: .now() + timeout, leeway: .seconds(0))
    self.connectTimer!.resume()
    
    opened by agildehaus 7
  • I am getting service list nil, when i disconnect & connect again, with out killing the app.

    I am getting service list nil, when i disconnect & connect again, with out killing the app.

    I can able to get services list when i connect firs time with peripheral,then disconnected > connect again, now iam unable to list out any services.

    opened by ashokKOMARA 6
  • How to get the state when the peripheral is disconnected?

    How to get the state when the peripheral is disconnected?

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) I need to get this callback when the peripheral is disconnected. How can I get this to work? Many thanks.

    opened by mobileguru121 5
  • Swift 5 & Fix warnings

    Swift 5 & Fix warnings

    Description

    • This pull-request contains Swift version update & warning fixes

    Motivation and Context

    • We'll have latest Swift version,

    • We'll solve language version related warnings

    after this pull-request merge.

    What kind of change does this PR introduce?

    • [x] Bug fix (non-breaking change which fixes an issue)
    • [x] File changes for improvement
    • [ ] New feature (non-breaking change which adds functionality)
    • [x] This change requires a documentation update
    opened by cci-emciftci 5
  • I can't able to set notification.

    I can't able to set notification.

    failure Error Domain=CBATTErrorDomain Code=10 "The attribute could not be found." UserInfo={NSLocalizedDescription=The attribute could not be found.}

    This is the error that aim getting while adding notification to peripheral characteristic. Can u please suggest me solution.

    opened by ashokKOMARA 5
  • Can someone help me on how I can get the correct value from Characteristics?

    Can someone help me on how I can get the correct value from Characteristics?

    I have the issue of every value in the Characteristics being "(null)" instead of the value of the device information I did put the serviceuuid as "180A" and it shows all the values as (null)

    opened by pjgapuz 0
  • CBATTErrorDomain code 131 on some devices

    CBATTErrorDomain code 131 on some devices

    Hi. I'm getting CBATTErrorDomain code 131, description "Unknown ATT error." I tried killing app in background/rebuild with Xcode but only 1 device worked, other would get above error.

    What's this error about? Is it related to device's settings? I read a topic here and the code is 132, but couldn't find anything helpful.

    Thank for any help.

    opened by tlquan2809 0
  • discoverServices error operationTimedOut(operation: EverShining.SBError.SBOperation.discoverServices)

    discoverServices error operationTimedOut(operation: EverShining.SBError.SBOperation.discoverServices)

    When the previous device is disconnected and another device is connected, the connection is successful, but an error is reported discoverServices

    The first connection is successful, discoverServices can be successful

    https://user-images.githubusercontent.com/13548479/188608830-b0b9e9c1-d6e9-42b6-8723-cd2d0fdec9f3.MP4

    opened by ljchen1129 2
  • Had to do a fix to solve service discovery timeouts

    Had to do a fix to solve service discovery timeouts

    For some reason the device discovery callback reported the same device multiple times.

    Every time it was discovered it was added to the array in this code:

                let peripheral = Peripheral(peripheral: peripheral)
                scanRequest.peripherals.append(peripheral)
    

    in

        func centralManager(_ central: CBCentralManager,
                            didDiscover peripheral: CBPeripheral,
                            advertisementData: [String: Any],
                            rssi RSSI: NSNumber)
    

    So I had the same device multiple times there and that created problems.

    When I took the first one out of that list (of the 6 pointing to the same device) I got timeouts when trying to discover its services. I presume because maybe the CBPeripheral instance was the same, but the PeripheralProxy wasn't, and inside that proxy you do cbPeripheral.delegate = self so it ends up pointing to an instance I am not using (if that all makes sense :) )

    I solved this by doing this:

            if scanRequest.peripherals.first (where: { $0.identifier == peripheral.identifier }) == nil {
                let peripheral = Peripheral(peripheral: peripheral)
                scanRequest.peripherals.append(peripheral)
                
                var rssiOptional: Int? = Int(truncating: RSSI)
                if let rssi = rssiOptional, rssi == 127 {
                    rssiOptional = nil
                }
                
                scanRequest.callback(.scanResult(peripheral: peripheral, advertisementData: advertisementData, RSSI: rssiOptional))
            }
    
    

    Just FYI ;)

    opened by ir-fuel 0
  • discoverCharacteristics 1 failure  -operationTimedOut(operation: SwiftyBluetooth.SBError.SBOperation.discoverServices)

    discoverCharacteristics 1 failure -operationTimedOut(operation: SwiftyBluetooth.SBError.SBOperation.discoverServices)

    Hello @jakerockland ,

    First of all thanks for such wonderful library which I'm using in my project currently i.e. SwiftyBluetooth. Theres one issue i'm facing in it, please kindly help me or you can suggest solution for it.

    I'm scanning for below CBUUIDs using SwiftyBluetooth.scanForPeripherals

    let serviceId3 = CBUUID(string:"FFF0") let serviceId4 = CBUUID(string:"FFB0")

    these service ids are used for detecting temperature devices.

    Ill get scanned devices successfully, after that I do successful connection with those peripheral using selectedPeripheral!.connect(withTimeout: 10)

    I'm calling func discoverServices for serviceId = CBUUID(string:"FFE0")

    After that the issue I'm getting in your library is

    discoverCharacteristics 1 failure -operationTimedOut(operation: SwiftyBluetooth.SBError.SBOperation.discoverServices)

    I did deep debugging in your library so I got to know that here in below code

    extension PeripheralProxy  {
    
    func discoverServices(_ serviceUUIDs: [CBUUID]?, completion: @escaping ServiceRequestCallback)  {
    
        self.connect { (result) in
            if let error = result.error {
                completion(.failure(error))
                return
            }
             
            if let serviceUUIDs = serviceUUIDs {
                let servicesTuple = self.cbPeripheral.servicesWithUUIDs(serviceUUIDs)
                
                if servicesTuple.missingServicesUUIDs.count == 0 {
                    completion(.success(servicesTuple.foundServices))
                    return
                }
            }
            
            let request = ServiceRequest(serviceUUIDs: serviceUUIDs) { result in
                completion(result)
            }
            
            self.serviceRequests.append(request)
            
            if self.serviceRequests.count == 1 {
                self.runServiceRequest()
            }
        }
    }
    

    In file PeripheralProxy.swift on line number 183 or in above code

    let servicesTuple = self.cbPeripheral.servicesWithUUIDs(serviceUUIDs)
                
     if servicesTuple.missingServicesUUIDs.count == 0 {
              completion(.success(servicesTuple.foundServices))
              return
     }
    

    Whenever I'm getting above timeout error at that time serviceTuple.missingServicesUUIDs.count is one.

    So whatever serviceId I've requested i.e serviceId = CBUUID(string:"FFE0") is missing. Kindly please let me know how can I resolve this, I'm trying to resolve this issue since last 3-4 days. Please help me. Thanks in advance.

    NOTE :- This is issue is not getting generated always its happening few times and sometimes serviceTuple.missingServicesUUIDs.count is zero and I'm getting success

    opened by kshrikant 2
Releases(3.0.1)
Owner
Jordane Belanger
Jordane Belanger
Closures based APIs for CoreBluetooth

SwiftyBluetooth Closures based APIs for CoreBluetooth. Features Replace the delegate based interface with a closure based interface for every CBCentra

Jordane Belanger 181 Jan 2, 2023
A small library that adds concurrency to CoreBluetooth APIs.

AsyncBluetooth A small library that adds concurrency to CoreBluetooth APIs. Features Async/Await APIs Queueing of commands Data conversion to common t

Manuel Fernandez 83 Dec 28, 2022
Simple, block-based, lightweight library over CoreBluetooth. Will clean up your Core Bluetooth related code.

LGBluetooth Simple, block-based, lightweight library over CoreBluetooth. Steps to start using Drag and Drop it into your project Import "LGBluetooth.h

null 170 Sep 19, 2022
CombineCoreBluetooth is a library that bridges Apple's CoreBluetooth framework and Apple's Combine framework

CombineCoreBluetooth is a library that bridges Apple's CoreBluetooth framework and Apple's Combine framework, making it possible to subscribe to perform bluetooth operations while subscribing to a publisher of the results of those operations, instead of relying on implementing delegates and manually filtering for the results you need.

Starry 74 Dec 29, 2022
iBeacons + CoreBluetooth

OWUProximityManager Detect and connect to nearby devices with iBeacons and CoreBluetooth. Sample Project To simulate functionality, select Client on o

David Ohayon 363 Dec 24, 2022
MbientLab 2 Feb 5, 2022
Replacement for Apple's Reachability re-written in Swift with closures

Reachability.swift Reachability.swift is a replacement for Apple's Reachability sample, re-written in Swift with closures. It is compatible with iOS (

Ashley Mills 7.7k Jan 1, 2023
Swifty closures for UIKit and Foundation

Closures is an iOS Framework that adds closure handlers to many of the popular UIKit and Foundation classes. Although this framework is a substitute f

Vinnie Hesener 1.7k Dec 21, 2022
A Swift micro-framework to easily deal with weak references to self inside closures

WeakableSelf Context Closures are one of Swift must-have features, and Swift developers are aware of how tricky they can be when they capture the refe

Vincent Pradeilles 72 Sep 1, 2022
A result builder that allows to define shape building closures

ShapeBuilder A result builder implementation that allows to define shape building closures and variables. Problem In SwiftUI, you can end up in a situ

Daniel Peter 47 Dec 2, 2022
A Swift micro-framework to easily deal with weak references to self inside closures

WeakableSelf Context Closures are one of Swift must-have features, and Swift developers are aware of how tricky they can be when they capture the refe

Vincent Pradeilles 72 Sep 1, 2022
Define and chain Closures with Inputs and Outputs

Closure Define and chain Closures with Inputs and Outputs Examples No Scoped State let noStateCount = Closure<String, String> { text in String(repea

Zach Eriksen 3 May 18, 2022
A property wrapper to enforce that closures are called exactly once!

A property wrapper that allows you to enforce that a closure is called exactly once. This is especially useful after the introduction of SE-0293 which makes it legal to place property wrappers on function and closure parameters.

Suyash Srijan 11 Nov 13, 2022
New Way Of Working Around With Closures.

Closured New Way Of Working Around With Closures. Are you tired of old-school callback closures? Are you always mess up with capturing references on a

Kiarash Vosough 8 Sep 18, 2022
Extensions which helps to convert objc-style target/action to swifty closures

ActionClosurable Usage ActionClosurable extends UIControl, UIButton, UIRefreshControl, UIGestureRecognizer and UIBarButtonItem. It helps writing swift

takasek 121 Aug 11, 2022
Swift extension which adds start, animating and completion closures for CAAnimation objects. Aka, CAAnimation + Closure / Block

Swift-CAAnimation-Closure Swift extension which adds start, animating and completion closures for CAAnimation objects. Aka, CAAnimation + Closure or C

HongHao Zhang 112 Jun 17, 2022
T - A simple testing framework using closures and errors

t Quickly test expectations What is t? t is a simple testing framework using clo

OpenBytes 6 Nov 7, 2022
Chain multiple UIView animations without endlessly nesting in completion closures.

⛓ Chain multiple UIView animations without endlessly nesting in completion closures. Used in some of the more superfluous animations in the OK Video app.

Pim 78 Dec 26, 2022
The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!

SwiftGen SwiftGen is a tool to automatically generate Swift code for resources of your projects (like images, localised strings, etc), to make them ty

null 8.3k Dec 31, 2022
The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!

SwiftGen SwiftGen is a tool to automatically generate Swift code for resources of your projects (like images, localised strings, etc), to make them ty

null 8.3k Jan 3, 2023