A dependency-free Swift package for talking to Android devices from macOS — browse storage and move files both ways over USB(MTP) and Wi-Fi(ADB), behind one async DeviceTransport abstraction.
- MTP over USB, spoken directly through
IOUSBHost— nolibmtp, no C dependencies. - ADB over Wi-Fi transport, with QR/pairing-code/IP pairing and mDNS auto-discovery.
- A single
DeviceTransportprotocol for both backends, plus aMockTransportfor tests and previews. async/awaitthroughout,Sendablevalue types, and a real-timeDeviceChangeevent stream.- Streaming up/download with progress reporting; handles files larger than 4 GB.
- USB reliability built in:data phases are terminated with a zero-length packet when their length is an exact multiple of the endpoint's max packet size(the classic "an upload of one specific size hangs forever" MTP failure), halted bulk endpoints are cleared automatically after I/O errors instead of wedging the whole connection, and write draining is cancellation-aware and time-bounded so a stuck device can't freeze the session.
- Android-only discovery:Apple devices expose PTP too(an iPhone's photo interface), so discovery skips them — an attached iPhone never shadows the actual Android phone.
- Zero third-party dependencies. Error messages localized in English and 繁體中文.
- macOS 15+
- Swift 6+(Xcode 16+)
Swift Package Manager:
.package(url: "https://github.com/5j54d93/MTPKit.git", from: "0.1.4")Then add "MTPKit" to your target's dependencies.
import MTPKit
// USB/MTP
guard let transport = await MTPTransport.discover() else { return }
let storages = try await transport.storages()
let root = try await transport.listChildren(of: nil, in: storages[0].id)
try await transport.download(node.id, to: localURL) { progress in
print(progress.fractionCompleted)
}Both USB and Wi-Fi backends conform to DeviceTransport, so they're interchangeable from the caller's side.
adbbinary(Wi-Fi only): MTPKit does not bundleadb.ADBClientlooks for it first in your app bundle(Bundle.main), then in common install locations(Homebrew、Android SDK), or you can pass an explicit path withADBClient(adbPath:). The USB/MTP path needs nothing extra.- Entitlements: USB access via
IOUSBHostrequires the consuming app to declare the appropriate USB device entitlement, and the ADB transport spawns theadbsubprocess — configure your app's sandbox/entitlements accordingly. A library can't carry these for you. - Tests: the hardware("live")tests no-op gracefully when no device is attached, so the suite stays green on CI/without a phone.
This package is MIT licensed.