Connecting a tvOS app to other devices over the local network (tvOS 16 / SwiftUI)

LocalDeviceManager

Using DeviceDiscoveryUI to connect an Apple TV app to an iPhone, iPad, or Apple Watch.

Limitations of DeviceDiscoveryUI

  • Only runs on Apple TV 4K currently (Apple TV HD is not supported)
  • The tvOS app can only connect to one device at a time (i.e. you couldn’t make a game with this that used two iPhones as controllers)
  • The tvOS app can only connect to other versions of your app that share the same bundle identifier (and are thus sold with Universal Purchase)
  • This will not work on either the tvOS or iOS simulators. You must use physical devices.

Usage

There are a few steps you need to run through in order to communicate between Apple TV and another device (I’ll use an iPhone for all examples but the same code would apply to iPad or Apple Watch).

Apple TV

Step 1.

Define supported devices with an NSApplicationServices key in your Info.plist

Step 2.

Instantiate the LocalDeviceManager using the application service key you defined in Step 1 (i.e. “remote” in this example):

@ObservedObject private var deviceManager = LocalDeviceManager(applicationService: "remote", didReceiveMessage: { data in
    guard let string = String(data: data, encoding: .utf8) else { return }
    NSLog("Message: \(string)")
}, errorHandler: { error in
    NSLog("ERROR: \(error)")
})

The LocalDeviceManager has a callback for when messages are received. You have access to the raw Data but I would suggest you only send encoded strings rather than custom models in case the packets are delivered in chunks.

An error handler is also provided in case of failures.

Step 3.

Present the Device Picker UI. This demo uses SwiftUI but you can use DDDevicePickerViewController in UIKit.

@State private var showDevicePicker = false

var body: some View {
    VStack {
        if deviceManager.isConnected {
            Button("Send") {
                deviceManager.send("Hello from tvOS!")
            }
            
            Button("Disconnect") {
                deviceManager.disconnect()
            }
        } else {
            DevicePicker(.applicationService(name: "remote")) { endpoint in
                deviceManager.connect(to: endpoint)
            } label: {
                Text("Connect to a local device.")
            } fallback: {
                Text("Device browsing is not supported on this device")
            } parameters: {
                .applicationService
            }
        }
    }
    .padding()
}

This will present the native device picker. Upon selecting a device, a notification will be sent asking the user to either download the app or open the app if installed. Once they do this, the connection will be established.

iPhone / iPad / Apple Watch

Step 1.

Declare the device can listen for connections by using an NSApplicationServices key in your Info.plist

Step 2.

Instantiate the LocalDeviceManager using the application service key you defined in Step 1 (i.e. “remote” in this example):

@ObservedObject private var deviceManager = LocalDeviceManager(applicationService: "remote", didReceiveMessage: { data in
    guard let string = String(data: data, encoding: .utf8) else { return }
    NSLog("Message: \(string)")
}, errorHandler: { error in
    NSLog("ERROR: \(error)")
})

This is identical to the tvOS implementation.

Step 3.

Create your UI and ensure that the LocalDeviceManager listener is created as soon as possible.

var body: some View {
    VStack {
        if deviceManager.isConnected {
            Text("Connected!")
            Button {
                deviceManager.send("Hello from iOS!")
            } label: {
                Text("Send")
            }
            Button {
                deviceManager.disconnect()
            } label: {
                Text("Disconnect")
            }
        } else {
            Text("Not Connected")
        }
    }
    .padding()
    .onAppear {
        try? deviceManager.createListener()
    }
}

You can now send data from tvOS to your connected device and vice versa.

Find Out More

You can read my blog post on this topic to learn more about DeviceDiscoveryUI in tvOS 16 and how I’m using this class in my own apps.

GitHub

View Github