14

Is there a delegate in Swift that would let my class know when new devices are plugged in via the computer's USB? I would like to know when a new device becomes available to my program.

Jacobo Koenig
  • 11,728
  • 9
  • 40
  • 75
  • Look for `NSWorkspace` Volume Mounting Notification User Info Keys. [link](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSWorkspace_Class/#//apple_ref/doc/constant_group/Volume_Mounting_Notification_User_Info_Keys) – Khundragpan Aug 17 '16 at 18:42
  • Actually, your code would be the delegate, whose methods would be called at various points in time. I'm not sure what the owner of a delegate is called, though – Alexander Aug 17 '16 at 18:46
  • Possible duplicate of http://stackoverflow.com/questions/34628464/how-to-implement-ioservicematchingcallback-in-swift/39662693 – jtbandes Dec 22 '16 at 17:47
  • I have created [USBDeviceSwift](https://github.com/Arti3DPlayer/USBDeviceSwift) library for convenient work with `IOKit.usb` and `IOKit.hid` – Arti Jun 29 '17 at 10:33
  • https://github.com/Jinjinov/Usb.Events/blob/master/Usb.Events/UsbEventWatcher.Mac.c – Jinjinov Apr 29 '20 at 07:29

2 Answers2

19

Eric Aya's answer is already quite good, but here's a Swift 3 adaptation. I wrapped most of the ugly stuff in a USBWatcher class; set yourself as the delegate of this object to receive notifications.

You can copy/paste the following into a playground to see it work — the example just logs a message to the console when devices are connected/disconnected.

It's unfortunate that the IOKit APIs haven't gotten the same Swift-ifying treatment that some other C APIs have been (e.g. CoreGraphics). io_name_t is a clunky tuple instead of a proper struct, the way C structs are usually imported to Swift; io_object_t isn't a real reference type, so it can't take advantage of ARC. Perhaps in the future this will change — if you'd like to see a better Swift API, you should file an enhancement request.

import Foundation
import IOKit
import IOKit.usb

public protocol USBWatcherDelegate: class {
    /// Called on the main thread when a device is connected.
    func deviceAdded(_ device: io_object_t)

    /// Called on the main thread when a device is disconnected.
    func deviceRemoved(_ device: io_object_t)
}

/// An object which observes USB devices added and removed from the system.
/// Abstracts away most of the ugliness of IOKit APIs.
public class USBWatcher {
    private weak var delegate: USBWatcherDelegate?
    private let notificationPort = IONotificationPortCreate(kIOMasterPortDefault)
    private var addedIterator: io_iterator_t = 0
    private var removedIterator: io_iterator_t = 0

    public init(delegate: USBWatcherDelegate) {
        self.delegate = delegate

        func handleNotification(instance: UnsafeMutableRawPointer?, _ iterator: io_iterator_t) {
            let watcher = Unmanaged<USBWatcher>.fromOpaque(instance!).takeUnretainedValue()
            let handler: ((io_iterator_t) -> Void)?
            switch iterator {
            case watcher.addedIterator: handler = watcher.delegate?.deviceAdded
            case watcher.removedIterator: handler = watcher.delegate?.deviceRemoved
            default: assertionFailure("received unexpected IOIterator"); return
            }
            while case let device = IOIteratorNext(iterator), device != IO_OBJECT_NULL {
                handler?(device)
                IOObjectRelease(device)
            }
        }

        let query = IOServiceMatching(kIOUSBDeviceClassName)
        let opaqueSelf = Unmanaged.passUnretained(self).toOpaque()

        // Watch for connected devices.
        IOServiceAddMatchingNotification(
            notificationPort, kIOMatchedNotification, query,
            handleNotification, opaqueSelf, &addedIterator)

        handleNotification(instance: opaqueSelf, addedIterator)

        // Watch for disconnected devices.
        IOServiceAddMatchingNotification(
            notificationPort, kIOTerminatedNotification, query,
            handleNotification, opaqueSelf, &removedIterator)

        handleNotification(instance: opaqueSelf, removedIterator)

        // Add the notification to the main run loop to receive future updates.
        CFRunLoopAddSource(
            CFRunLoopGetMain(),
            IONotificationPortGetRunLoopSource(notificationPort).takeUnretainedValue(),
            .commonModes)
    }

    deinit {
        IOObjectRelease(addedIterator)
        IOObjectRelease(removedIterator)
        IONotificationPortDestroy(notificationPort)
    }
}

extension io_object_t {
    /// - Returns: The device's name.
    func name() -> String? {
        let buf = UnsafeMutablePointer<io_name_t>.allocate(capacity: 1)
        defer { buf.deallocate(capacity: 1) }
        return buf.withMemoryRebound(to: CChar.self, capacity: MemoryLayout<io_name_t>.size) {
            if IORegistryEntryGetName(self, $0) == KERN_SUCCESS {
                return String(cString: $0)
            }
            return nil
        }
    }
}


import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

class Example: USBWatcherDelegate {
    private var usbWatcher: USBWatcher!
    init() {
        usbWatcher = USBWatcher(delegate: self)
    }

    func deviceAdded(_ device: io_object_t) {
        print("device added: \(device.name() ?? "<unknown>")")
    }

    func deviceRemoved(_ device: io_object_t) {
        print("device removed: \(device.name() ?? "<unknown>")")
    }
}

let example = Example()
jtbandes
  • 115,675
  • 35
  • 233
  • 266
  • Awesome answer, thanks a lot! I'm not sure I really understand the memory management here, for example `let watcher = Unmanaged.fromOpaque(instance!).takeUnretainedValue()` is a bit mysterious to me. // Anyway, that's really great, and I specifically appreciate having a Playground example. I will study this kind of memory management using this answer as an example. Thanks again! :) – Eric Aya Dec 22 '16 at 10:54
  • 2
    @EricAya: Opaque pointers are a way to "tunnel" a reference to an instance of a class through C functions. It is often used in connection with functions/closures which are used as C callbacks and cannot capture context. Compare (and I apologize for the self-advertisement) http://stackoverflow.com/questions/33294620/how-to-cast-self-to-unsafemutablepointervoid-type-in-swift, and http://stackoverflow.com/questions/33260808/swift-proper-use-of-cfnotificationcenteraddobserver-w-callback for another example which demonstates the usage. – Martin R Dec 22 '16 at 15:37
  • @MartinR Those `bridge()` helpers are pretty cool! I hadn't thought of doing that. Overloading makes that quite nice. – jtbandes Dec 22 '16 at 16:47
  • 3
    I have created [USBDeviceSwift](https://github.com/Arti3DPlayer/USBDeviceSwift) library for convenient work with `IOKit.usb` and `IOKit.hid` – Arti Jun 29 '17 at 10:32
  • This is the only code that worked for me, Swift 5, Xcode 11.5. Thanks@ jtbandes! – Dohab Jun 02 '20 at 17:13
  • @Arti please update your files, they are not detecting anything! – Dohab Jun 02 '20 at 17:14
  • @jtbandes does `Example()` always have to go at the beginning of `class AppDelegate` ? The methods I need to fire in `deviceAdded()` are in another ViewController and this is where I'm stuck. – Mikeumus Apr 03 '21 at 18:51
  • 1
    It shouldn't matter. The notification listener will be tied to the lifetime of the USBWatcher object, since it automatically unregisters itself in `deinit`. – jtbandes Apr 03 '21 at 19:25
3

This answer worked for me https://stackoverflow.com/a/35788694 but it needed some adaptation, like creating a bridging header to import some specific IOKit parts.

First, add IOKit.framework to your project (click "+" in "Linked Frameworks and Libraries").

Then create a new empty ".m" file, whatever its name. Xcode will then ask if it should make a "bridging header". Say YES.

Ignore the ".m" file. In the new "YOURAPPNAME-Bridging-Header.h" file that Xcode just created, add the following lines:

#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/hid/IOHIDKeys.h>

Now you can use the code in the linked answer. Here's a simplified version:

class USBDetector {
    class func monitorUSBEvent() {
        var portIterator: io_iterator_t = 0
        let matchingDict = IOServiceMatching(kIOUSBDeviceClassName)
        let gNotifyPort: IONotificationPortRef = IONotificationPortCreate(kIOMasterPortDefault)
        let runLoopSource: Unmanaged<CFRunLoopSource>! = IONotificationPortGetRunLoopSource(gNotifyPort)
        let gRunLoop: CFRunLoop! = CFRunLoopGetCurrent()
        CFRunLoopAddSource(gRunLoop, runLoopSource.takeRetainedValue(), kCFRunLoopDefaultMode)
        let observer = UnsafeMutablePointer<Void>(unsafeAddressOf(self))
        _ = IOServiceAddMatchingNotification(gNotifyPort,
                                              kIOMatchedNotification,
                                              matchingDict,
                                              deviceAdded,
                                              observer,
                                              &portIterator)
        deviceAdded(nil, iterator: portIterator)
        _ = IOServiceAddMatchingNotification(gNotifyPort,
                                              kIOTerminatedNotification,
                                              matchingDict,
                                              deviceRemoved,
                                              observer,
                                              &portIterator)
        deviceRemoved(nil, iterator: portIterator)
    }
}

func deviceAdded(refCon: UnsafeMutablePointer<Void>, iterator: io_iterator_t) {
    var kr: kern_return_t = KERN_FAILURE
    while case let usbDevice = IOIteratorNext(iterator) where usbDevice != 0 {
        let deviceNameAsCFString = UnsafeMutablePointer<io_name_t>.alloc(1)
        defer {deviceNameAsCFString.dealloc(1)}
        kr = IORegistryEntryGetName(usbDevice, UnsafeMutablePointer(deviceNameAsCFString))
        if kr != KERN_SUCCESS {
            deviceNameAsCFString.memory.0 = 0
        }
        let deviceName = String.fromCString(UnsafePointer(deviceNameAsCFString))
        print("Active device: \(deviceName!)")
        IOObjectRelease(usbDevice)
    }
}

func deviceRemoved(refCon: UnsafeMutablePointer<Void>, iterator: io_iterator_t) {
    // ...
}

Note: deviceAdded and deviceRemoved need to be functions (not methods).

To use this code, just launch the observer:

USBDetector.monitorUSBEvent()

This will list the currently plugged devices, and on every new USB device plug/unplug event it will print the device name.

Community
  • 1
  • 1
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
  • 1
    This is written for Swift 2.2? I'm trying to get this working in Swift 3, but to no avail :/ Any ideas on the changes that would need to be made? – simonthumper Sep 21 '16 at 22:45
  • I've set a bounty on this question - let's hope we'll get an interesting Swift 3 answer. – Eric Aya Dec 21 '16 at 20:32
  • @EricAya: Did you notice that the Q&A that you reference has an answer for Swift 3 as well? – Martin R Dec 22 '16 at 15:45
  • @MartinR No, I missed that, thanks. No problem though, I'm super happy with jtbandes' answer (awesome code) and will use it to study interactions with C and USB in Swift 3. I think I would have put a bounty here anyway even if I had seen this earlier, I was curious to see how someone would write a good Swift 3 version for this specific question. – Eric Aya Dec 22 '16 at 15:56
  • I have created [USBDeviceSwift](https://github.com/Arti3DPlayer/USBDeviceSwift) library for convenient work with `IOKit.usb` and `IOKit.hid` – Arti Jun 29 '17 at 10:32