// Custom class
public protocol BluetoothManagerProtocol {
var delegate: CBCentralManagerDelegate? {get set}
//var state: CBCentralManagerState { get }
func scanForPeripheralsWithServices(serviceUUIDs: [CBUUID]?, options: [String : AnyObject]?)
func stopScan()
func connectPeripheral(peripheral: CBPeripheral, options: [String : AnyObject]?)
func connectPeripheral(peripheral: BluetoothPeripheral, options: [String : AnyObject]?)
}
extension CBCentralManager : BluetoothManagerProtocol {
public func connectPeripheral(peripheral: CBPeripheral, options: [String : AnyObject]?) {
//
}
public func scanForPeripheralsWithServices(serviceUUIDs: [CBUUID]?, options: [String : AnyObject]?) {
//
}
public func connectPeripheral(peripheral: BluetoothPeripheral, options: [String : AnyObject]?) {
guard let peripheral = peripheral as? CBPeripheral else {
return
}
connectPeripheral(peripheral, options: options)
}
}
extension CBCentralManagerDelegate{
func centralManager(central: BluetoothManagerProtocol, didDiscoverPeripheral peripheral: BluetoothPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) {}
func centralManager(central: BluetoothManagerProtocol,didConnectPeripheral peripheral:BluetoothPeripheral) {}
func centralManagerDidUpdateState(central: BluetoothManagerProtocol) {}
func centralManager(central: BluetoothManagerProtocol, didDisconnectPeripheral peripheral: BluetoothPeripheral, error: NSError?) {}
}
I just jumped in to advanced topics of Protocols and Delegates in Swift.I am surprised, can a CBCentralManager extend custom Protocol?
How can CBCentralManagerDelegate methods can be parametrized with the custom Protocol?
Whats the concept behind this? And what exactly is the need?
This was written in swift 2.3. Will this strategy work in Swift 4.0?
Yes, CBCentralManager is from Core Bluetooth framework and can be made to include custom protocol definitions. This approach is followed to leverage TDD - Test Driven Development.
As unit testing Bluetooth functionality makes it difficult while syncing devices, developers take advantage of dependency injection to mock methods by creating their own custom methods instead of using the methods provided by Apple for iOS Frameworks.
You can include your own custom methods for UIView, UIColor etc..
For example
class MockCBCentralManager : BluetoothManagerProtocol {
var delegate: CBCentralManagerDelegate?
var scanCalled: Bool = false
var connectPeripheralCalled = false
fileprivate var internalState: CBCentralManagerState = .unknown
var state: CBCentralManagerState {
get {
return internalState
}
}
}
func scanForPeripheralsWithServices(_ serviceUUIDs: [CBUUID]?, options[String : AnyObject]?)
{
scanCalled = true
let advertisementData =
[CBAdvertisementDataServiceUUIDsKey :
[STUDENT_APP_UUID],CBAdvertisementDataLocalNameKey:"MockPeripheral"]
let mock = MockPeripheral()
(delegate as? CentralManager)?.centralManager(self,
didDiscoverPeripheral: mock, advertisementData:
advertisementData,RSSI: 90)
}
More information can be found at
https://nomothetis.svbtle.com/the-ghost-of-swift-bugs-future
Cheers!
Related
I am working on a project for scanning BLE devices all around in the foreground and there is no issue. It is working well!
But background I cannot make scanning. I did everything on info.plist and on the section "Signing&Capabilities", switch on the Background Model for "Background Fetch".
I read almost everything on the Web about Corebluetooth, BGAppRefreshTask, Background Modes but find nothing.
Only thing I find get advertisement data of the BLE devices around me. UUID and other things are not important for me. I only need to identify BLE devices and know that it is same in other iOS device when it also look for it.
If you have any other solution to guide me to go another way, it will appreciated.
Here is the code of BLECentral. The .swift file which I want to call in Background.
import Foundation
import CoreBluetooth
class BLECentral: NSObject, CBCentralManagerDelegate {
var manager: CBCentralManager!
var discoveredPeripherals = [DiscoveredPeripheral]()
var onDiscovered: (()->Void)?
override init() {
super.init()
manager = CBCentralManager(delegate: self, queue: nil)
}
func scanForPeripherals() {
let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: false]
manager.scanForPeripherals(withServices: nil, options: options)
print("ben tarıyom")
}
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
scanForPeripherals()
} else {
print("central is unavailable: \(central.state.rawValue)")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if let existingPeripheral = discoveredPeripherals.first(where: {$0.peripheral == peripheral}) {
existingPeripheral.advertisementData = advertisementData
existingPeripheral.rssi = RSSI
print(existingPeripheral.advertisementData)
} else {
discoveredPeripherals.append(DiscoveredPeripheral(peripheral: peripheral, rssi: RSSI, advertisementData: advertisementData))
}
onDiscovered?()
}
}
Here is the AppDelegate code:
import UIKit
import CoreBluetooth
import BackgroundTasks
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
var central: BLECentral!
var manager: CBCentralManager!
var window: UIWindow?
var flowController: AppFlowController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
flowController = AppFlowController(window: window!)
flowController?.start()
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.blebacktrial.refresh", using: nil) { (task) in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
func handleAppRefresh(task: BGAppRefreshTask) {
central.scanForPeripherals()
scheduleAppRefresh()
}
// MARK: - Scheduling Tasks
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.blebacktrial.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 60) // Fetch no earlier than 1 minutes from now
do {
try BGTaskScheduler.shared.submit(request)
print("task scheduled")
} catch {
print("Could not schedule app refresh: \(error)")
}
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
}
You don't need to use a background refresh task. Scanning for BLE peripherals is a supported background mode in its own right.
However, you can't scan for service nil in the background. You must scan for the specific service that you are interested in.
From the documentation
Your app can scan for Bluetooth devices in the background by specifying the bluetooth-central background mode. To do this, your app must explicitly scan for one or more services by specifying them in the serviceUUIDs parameter. The CBCentralManager scan option has no effect while scanning in the background.
I want to get notified when something happened at the BLE device.
If BLE device passes some data/Command to the app, then in-app the advertisementData not changed.
But the same thing we can do with android it's working perfectly.
I want to implement functionality like when advertisementData changed I want to get notify.
Please help me to implement this.
Below is my AppDelegate.swift class.
private var centralManager : CBCentralManager!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
print("entering foreground...")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("entered background...")
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Bluetooth is On")
let kTrackStandardDeviceInfoServiceUUID = CBUUID(string: "180A")
let dictionaryOfOptions = [CBCentralManagerScanOptionAllowDuplicatesKey : true]
let arrayOfServices: [CBUUID] = [kTrackStandardDeviceInfoServiceUUID]
centralManager.scanForPeripherals(withServices: arrayOfServices, options: dictionaryOfOptions)
} else {
print(central.state)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("\nName : \(peripheral.name ?? "(No name)")")
print("RSSI : \(RSSI)")
let name = peripheral.name ?? ""
if name.contains("ETGuardian") {
let DetailData = advertisementData["kCBAdvDataManufacturerData"]
let DiscoveredData = String(describing: DetailData)
print(DiscoveredData)
for ad in advertisementData {
print("AD Data: \(ad)")
}
}
}
For having the application can work in the background, you need to implement some background services in your apps.
Usually, background service is location fetch.
Please note that background service will make your app draining the battery faster
To implement the background service, Click your project, Signing & Capabilities, Background Modes, enable the BLE features.
I want to scan BLE devices on the background as like a foreground. But my iOS app doesn't work as I expect. Below is my AppDelegate class code.
private var centralManager : CBCentralManager!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
print("entering foreground...")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("entered background...")
print(centralManager.state)
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Bluetooth is On")
centralManager.scanForPeripherals(withServices: nil, options: nil)
} else {
print(central.state)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("\nName : \(peripheral.name ?? "(No name)")")
print("RSSI : \(RSSI)")
let name = peripheral.name ?? ""
if name.contains("ETGuardian") {
let DetailData = advertisementData["kCBAdvDataManufacturerData"]
let DiscoveredData = String(describing: DetailData)
print(DiscoveredData)
for ad in advertisementData {
print("AD Data: \(ad)")
}
}
}
}
Please help me to scan app in background state as like a foreground.
From Docs: link
Apps that have specified the bluetooth-central background mode are
allowed to scan while in the background. That said, they must
explicitly scan for one or more services by specifying them in the
serviceUUIDs parameter. The CBCentralManager scan option is ignored
while scanning in the background.
For background scanning to work you need to specify serviceUUIDs.
I'm studying Core Bluetooth framework and I did a test project about it for learning:
class ViewController: UIViewController,CBCentralManagerDelegate,CBPeripheralDelegate {
var centralManager: CBCentralManager = CBCentralManager()
var peripheral: CBPeripheral? = nil
override func viewDidLoad()
{
super.viewDidLoad()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
func centralManagerDidUpdateState(_ central: CBCentralManager)
{
central.scanForPeripherals(withServices: nil, options: nil)
}
#available(iOS 5.0, *)
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)
{
let device = (advertisementData as NSDictionary)
.object(forKey: CBAdvertisementDataLocalNameKey)
as? NSString
let isMyIphone = device?.contains("iPhone")
}
}
For starting I would like to see Bluetooth name around me, for this reason I have 2 iPhone.
One I use for execute this 'app' for scanning and I would like to see the name of the other iPhone (in Bluetooth setting is name is 'iPhone'), but when I start scanning the method 'didDiscover' is called but the device constant is nil.
Why? What I wrong?
is better if you use
peripheral.name
instead
let device = (advertisementData as NSDictionary)
.object(forKey: CBAdvertisementDataLocalNameKey)
as? NSString
I'm implementing simple one-line communication between iOS app and watchKit with sendMessage.
The issue is - the communication is continuous.
The user presses a button on appleWatch, same action happens on Phone e.t.c.
Unfortunately, after two or three actions - the iOS app stops responding.
As advised by some on the forums - i've implemented backgroundTaskWithExpirationHandler, however, it doesn't work (doesn't give me even a promised three minute timeout).
Here's my code in the iOS app AppDelegate
func session(session: WCSession, didReceiveMessage message: [String : AnyObject]) {
//recieve messages from watch
print(message["b"]! as? String)
let sweetN = message["b"]! as? String
//var sweetB = message["abs"]! as? [Int : Bool]
dispatch_async(dispatch_get_main_queue(), {
let taskID = self.beginBackgroundUpdateTask()
if sweetN == "peeks"{
if WCSession.isSupported(){
let message = [ "gettingData": "datareceived" ]
session.sendMessage(message, replyHandler: { replyDict in }, errorHandler: { error in })
}
}
self.endBackgroundUpdateTask(taskID)
})
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
This worked for me.
You have to set the WCSession in the app delegate like:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
replyHandler(["message": "received!"])
}
and in the apple watch: the file extension delegate.swift do the same:
override init() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}