Nearby Bluetooth devices using Swift 3.0
Asked Answered
C

1

12

I'm looking for a way to programmatically list any nearby Bluetooth devices (discoverable) that my device finds. I have not been able to find any information or tutorials regarding performing this call in Swift 3.0. This Q-A post discusses finding these devices using Swift 1.0 and building in Xcode 6, rather than the latest version 8.

I did my best to try to make my code into the 3.0 Syntax from the 1.0, but while running the following code, nothing is returned in the Playground:

import Cocoa
import IOBluetooth
import PlaygroundSupport

class BlueDelegate : IOBluetoothDeviceInquiryDelegate {
    func deviceInquiryComplete(_ sender: IOBluetoothDeviceInquiry, error: IOReturn, aborted: Bool) {
        aborted
        print("called")
        let devices = sender.foundDevices()
        for device : Any? in devices! {
            if let thingy = device as? IOBluetoothDevice {
                thingy.getAddress()
            }
        }
    }
}

var delegate = BlueDelegate()
var inquiry = IOBluetoothDeviceInquiry(delegate: delegate)
inquiry?.start()
PlaygroundPage.current.needsIndefiniteExecution = true
Cartload answered 16/11, 2016 at 16:3 Comment(8)
Are you failing to see devices that do show up in a nearby iPhone or Mac's Bluetooth panel as discoverable? The Mac doesn't provide a general purpose Bluetooth sniffer.Mortification
So these are all devices that do show up as discoverable by some other nearby device, but the above code doesn't see them? (When you say "after a button is pressed" do you mean "the button that makes the device discoverable" or do you mean "a button in my UI?")Mortification
It doesn't matter if the device doing the search is discoverable. It matters if the devices you want to detect are currently discoverable. You will know this if they show up in other Bluetooth preference panels in iOS or Mac. If they're not showing up there, they're very unlikely to show up here.Mortification
Unless you specifically need this to work in a Playground, just put it in an application. It works fine in an application. For a playground, you'd at least need PlaygroundPage.current.needsIndefiniteExecution = true (you didn't set it to true). Failure to figure out Bluetooth on Mac certainly doesn't make you an idiot; it's quite challenging.Mortification
The above worked for me without that if you put it in an application rather than a Playground. Tricking async stuff to work in a Playground is fragile and IMO generally more trouble than it's worth, but if that's the goal, then whatever works for you. Just don't think you need it in a real application.Mortification
Look at CoreBluetooth. I think what you're describing will be dramatically simpler using CBCentral and CBPeripheral. It's designed exactly to address the kind of problem you're describing very efficiently.Mortification
developer.apple.com/videos/play/wwdc2012/703 developer.apple.com/videos/play/wwdc2013/703 This doesn't really lend itself to StackOverflow. Watch the videos, play a little w/ CoreBluetooth, and contact me at [email protected] if you find yourself stuck. Bluetooth is a somewhat complicated beast.Mortification
Thought I'd share that I implemented it properly in my answer below: @RobNapierCartload
C
11

Using IOBluetooth the Correct Way

The following code works flawlessly in Xcode Version 8.2.1 (8C1002), Swift 3.0. There are a few lines that aren't required, such as the entire method of deviceInquiryStarted.

Update: These usages still work as of Xcode 9.2 (9B55) and Swift 4.

Playground

import Cocoa
import IOBluetooth
import PlaygroundSupport
class BlueDelegate : IOBluetoothDeviceInquiryDelegate {
    func deviceInquiryStarted(_ sender: IOBluetoothDeviceInquiry) {
        print("Inquiry Started...")
//optional, but can notify you when the inquiry has started.
    }
    func deviceInquiryDeviceFound(_ sender: IOBluetoothDeviceInquiry, device: IOBluetoothDevice) {
        print("\(device.addressString!)")
    }
    func deviceInquiryComplete(_ sender: IOBluetoothDeviceInquiry!, error: IOReturn, aborted: Bool) {
//optional, but can notify you once the inquiry is completed.
    }
}
var delegate = BlueDelegate()
var ibdi = IOBluetoothDeviceInquiry(delegate: delegate)
ibdi?.updateNewDeviceNames = true
ibdi?.start()
PlaygroundPage.current.needsIndefiniteExecution = true

Project-Application Usage

import Cocoa
import IOBluetooth
import ...
class BlueDelegate : IOBluetoothDeviceInquiryDelegate {
    func deviceInquiryStarted(_ sender: IOBluetoothDeviceInquiry) {
        print("Inquiry Started...")
}
    func deviceInquiryDeviceFound(_ sender: IOBluetoothDeviceInquiry, device: IOBluetoothDevice) {
        print("\(device.addressString!)")
    }
}

//other classes here:



//reference the following outside of any class:
var delegate = BlueDelegate()
var ibdi = IOBluetoothDeviceInquiry(delegate: delegate)

//refer to these specifically inside of any class:
ibdi?.updateNewDeviceNames = true
ibdi?.start() //recommended under after an action-button press.

Explanation

The issue I was originally faced with was trying to access the information as the inquiry was still in process.

When I accessed it, under many different occasions my playground would hang and I would be forced to force quit both Xcode.app, and com.apple.CoreSimulator.CoreSimulatorService from the Activity Monitor. I lead myself to believe that this was just a Playground bug, only to learn that my application would crash once the inquiry finished.

As Apple's API Reference states:

Important Note: DO NOT perform remote name requests on devices from delegate methods or while this object is in use. If you wish to do your own remote name requests on devices, do them after you have stopped this object. If you do not heed this warning, you could potentially deadlock your process.

Which entirely explained my issue. Rather than directly asking for the IOBluetoothDevice information from the sender.foundDevices() method (which I believe may not have been updating..?) I simply used the parameters built into the function to mention that it was indeed an IOBluetoothDevice object, and simply to ask for that information to be printed.

Final Note

I hope that this Q/A I've created helps others in need when using IOBluetooth in Swift. The lack of any tutorials and the high amounts of outdated, Objective-C code made finding this information very challenging. I'd like to thank @RobNapier for the support on trying to find the answer to this riddle in the beginning. I'd also like to thank NotMyName for the reply on my post on the Apple Developer Forums.

I will be exploring the usage of this in an iOS device more sooner than later!

Cartload answered 30/1, 2017 at 17:5 Comment(6)
I haven't tested yet but this is a very nice answer. Congrats and thanks, this will probably be useful.Malaguena
this code doesn't seem to work for me. It never returns anything. I also tried setting ibdi.searchType to kIOBluetoothDeviceSearchLE.rawValue or kIOBluetoothDeviceSearchClass.rawValue and still nothing. Apple's docs do menthing that " It will not let you perform unlimited back-to-back inquiries, but will instead throttle the number of attempted inquiries if too many are attempted within a small window of time" but that seems VERY frustrating for when you're developing a tool. And there seems to be no way to check if you're throttled. Any ideas?Drucilla
Are you in a playground environment or in a project? It seems to be working fine in a playground. @DrucillaCartload
ok, what the hell is a playground :D and yeah, not working for me too (just trying to build a mac app in normal environment), even the "Inquiry Started" is not being called... am I missing some permissions in plist or what?Dipterous
ah, bloody hell - turns out on macOS you need to add a "Capability: App Sandbox" to your projects target, and in that sandbox, turn on "Bluetooth", then everything magically started working...Dipterous
good to hear, let me know if you need any more help @DipterousCartload

© 2022 - 2024 — McMap. All rights reserved.