1

WorkAround:

I am creating a utility class let's say BLEScanManager, which is responsible to scan nearby BLE devices. The only job of the utility class is to scan BLE devices and make a list of it.

The other classes can create an object of this BLEScanManager and get an array of BLE devices, like [Bluetooth] (here Bluetooth is a custom modal class).

Now to scan BLE devices, I have created the extension of BLEScanManager in the same class and override its delegate methods like below:

import UIKit
import CoreBluetooth

protocol BLEScanDelegate {
   func reloadDeviceList()
}

internal class BLEScanManager: NSObject {

    private var centralManager: CBCentralManager?
    var devices : [Bluetooth] = []
    var delegate: BLEScanDelegate?

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: .main)
    }

    // MARK:- Custom methods
    func isScanning() -> Bool {
        return centralManager?.isScanning ?? false
    }

    func stopScanning() {
        centralManager?.stopScan()
    }

    func startScanning() {
        devices.removeAll()
        let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey:
            NSNumber(value: false)]
        centralManager?.scanForPeripherals(withServices: nil, options: options)
    }
}

extension BLEScanManager : CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
            case .unknown:      print("central.state is .unknown")
            case .resetting:    print("central.state is .resetting")
            case .unsupported:  print("central.state is .unsupported")
            case .unauthorized: print("central.state is .unauthorized")
            case .poweredOff:   print("central.state is .poweredOff")
            case .poweredOn:    print("central.state is .poweredOn")
                                self.startScanning()
            @unknown default:   fatalError("unknow state")
        }
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        print("Discovered \(peripheral)")
        var bluetooth = Bluetooth()
        bluetooth.name = peripheral.name
        bluetooth.identifier = peripheral.identifier.uuidString
        bluetooth.state = peripheral.state.rawValue
        bluetooth.advertisementData = advertisementData
        let power = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? Double
        let value: Double = pow(10, ((power ?? 0 - Double(truncating: RSSI))/20))
        bluetooth.signalStrength = String(describing: value.round)

        // Do not add duplicate device
        let fitlerArray : [Bluetooth] = devices.filter { $0.identifier ==  bluetooth.identifier}
        if fitlerArray.count == 0 {
            devices.append(bluetooth)
        }
        self.delegate?.reloadDeviceList()
    }
}

The thing here is these methods are exposed in the other classes too.

For example:

I have created an object of BLEScanManager in other class BLEListViewController to show a list of BLE device in UITableView.

class BLEListViewController: UITableViewController {
     var scanManager: BLEScanManager!
}

I can access CBCentralManagerDelegate delegate methods in BLEListViewController class using an object scanManager.

Like below,

self.scanManager.centralManager?(<#T##central: CBCentralManager##CBCentralManager#>, didConnect: <#T##CBPeripheral#>)

This should expose the internal utility delegate methods to the outside world.

Question is how to stop exposing these delegates?

Please note that, if I use internal keyword it only hides that specific method. But it still allows accessing all other CBCentralManagerDelegate methods.

Kampai
  • 22,848
  • 21
  • 95
  • 95
  • Not sure if it works but have you tried `private extension BLEScanManager : CBCentralManagerDelegate`? – Tj3n Sep 03 '19 at 10:26
  • I have previously used [SwiftyBluetooth Library](https://github.com/jordanebelanger/SwiftyBluetooth) which allowed me to change the core bluetooth using delegate methods to Closure based methods. Perhaps this can help you – chirag90 Sep 03 '19 at 10:27
  • @Tj3n: Yes I tried with it, it gives me an error: 'private' modifier cannot be used with extensions that declare protocol conformances – Kampai Sep 03 '19 at 10:28
  • @chirag90: Thanks, let me check that too. – Kampai Sep 03 '19 at 10:28
  • To make it more clear, I have added all the code of utility class – Kampai Sep 03 '19 at 10:52

3 Answers3

3

There’s nothing you can do about this. It is a problem with all optional protocol methods. The trouble is that optional protocol methods are an Objective C feature. You cannot hide such a method as private, because now Objective C cannot see it and so the method will never be discovered and called. It’s a flaw in the way Swift interfaces with Objective C. You just have to live with it.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I was wondering if we can do so to make it. But I guess thats not possible. Thanks for the details explanation, let's hope we can get a way in future. – Kampai Sep 03 '19 at 10:59
1

What you need to do, is to hide behind a protocol - see the following example.

protocol Exposed {
 func run()
}

struct Bluetooth {
 func hidden() {
  print("This method is hidden - ish")
 }
}

extension Bluetooth: Exposed {
 func run() {
  print("this method is exposed")
 }
}

struct BluetoothScanManager {
 static func scan() -> [Exposed] {
  return [Bluetooth(), Bluetooth()]
 }
}

Now, when you run BluetoothScanManager you will get a list of exposed only with the run() method available, but behind the code it is a Bluetooth

for exposed in BluetoothScanManager.scan() {
 exposed.run()
 exposed.hidden() // not possible
} 
magnuskahr
  • 1,137
  • 9
  • 17
  • Very appreciated your help, but the thing is the delegates are `CBCentralManagerDelegate` which is not controlled by us. Your answer will work if we are using custom delegate methods. But how we can apply above policy to `CBCentralManagerDelegate` methods? – Kampai Sep 03 '19 at 10:46
  • @Kampai may be use an internal class which confirms to CBCentralManagerDelegate. – Varun Malhotra Sep 03 '19 at 10:48
  • @Kampai when the class is internal make the instance private also. when it's access is private it shouldn't be visible/accessible to outside world – Varun Malhotra Sep 03 '19 at 10:50
  • Maybe I have gotten the wrong idea of your problem. But you can still hide behind an interface, and simply dont allow the creation of BLEScanManager, but let it have a method like: ``` struct BLEScanManager { static func createManager() -> Exposed { return BLEScanManager() } private init() {} } extension BLEScanManager: Exposed { ... } – magnuskahr Sep 03 '19 at 10:53
  • @magnuskahr: Can you please edit your answer and provide these comments details in the answer, so that I can understand it. – Kampai Sep 03 '19 at 11:01
0

Your only way to do this (which also is the correct way in my opinion) is to create a new private object inside the BLEScanManager class that implements the CBCentralManagerDelegate.

Than you can pass this object to the CBCentralManager init method.

If you then need to comunicate from this object to the BLEScanManager you can simply use another delegate with a private protocol so it doesn't show those methods outside of the class.

It would look something like this:


import UIKit
import CoreBluetooth

protocol BLEScanDelegate {
   func reloadDeviceList()
}



internal class BLEScanManager: NSObject {

    private var centralManager: CBCentralManager?
    var devices : [Bluetooth] = []
    var delegate: BLEScanDelegate?
    let centralManagerDelegate = MyCentralManagerDelegateImplementation()
    override init() {
        super.init()
        centralManagerDelegate.delegate = self
        centralManager = CBCentralManager(delegate: centralManagerDelegate, queue: .main)
    }

    // MARK:- Custom methods
    func isScanning() -> Bool {
        return centralManager?.isScanning ?? false
    }

    func stopScanning() {
        centralManager?.stopScan()
    }

    func startScanning() {
        devices.removeAll()
        let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey:
            NSNumber(value: false)]
        centralManager?.scanForPeripherals(withServices: nil, options: options)
    }
}

private extension BLEScanManager: CentralManagerEventsDelegate {
    func didUpdateState(){
        // Here goes the switch with the startScanning
    }


    func didDiscoverPeripheral(...) {
        // Here goes the logic on new peripheral and the call on self.delegate?.reloadDeviceList
    }

}

internal protocol CentralManagerEventsDelegate: class {
    func didUpdateState()
    func didDiscoverPeripheral(...)
}

internal class MyCentralManagerDelegateImplementation: NSObject, CBCentralManagerDelegate {

    weak var delegate: CentralManagerEventsDelegate?

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        self.delegate?.didUpdateState()
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        self.delegate?.didDiscoverPeripheral(actualParams)
    }
}
Enricoza
  • 1,101
  • 6
  • 18