├── LocalDeviceManager.swift ├── README.md ├── Remote.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── RemoteTV.xcscheme └── xcuserdata │ └── bendodson.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── RemotePhone ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── RemotePhoneApp.swift └── RemoteTV ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── App Icon & Top Shelf Image.brandassets │ ├── App Icon - App Store.imagestack │ │ ├── Back.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Front.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Middle.imagestacklayer │ │ │ ├── Content.imageset │ │ │ └── Contents.json │ │ │ └── Contents.json │ ├── App Icon.imagestack │ │ ├── Back.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Front.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Middle.imagestacklayer │ │ │ ├── Content.imageset │ │ │ └── Contents.json │ │ │ └── Contents.json │ ├── Contents.json │ ├── Top Shelf Image Wide.imageset │ │ └── Contents.json │ └── Top Shelf Image.imageset │ │ └── Contents.json └── Contents.json ├── ContentView.swift ├── Info.plist ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json └── RemoteApp.swift /LocalDeviceManager.swift: -------------------------------------------------------------------------------- 1 | // Developed by Ben Dodson (ben@bendodson.com) 2 | 3 | import SwiftUI 4 | import Network 5 | 6 | class LocalDeviceManager: ObservableObject { 7 | 8 | public private(set) var applicationService: String 9 | public var didReceiveMessage: ((Data) -> Void)? 10 | public var errorHandler: ((Error) -> Void)? 11 | public private(set) var minimumIncompleteLength: Int 12 | public private(set) var maximumLength: Int 13 | 14 | private var listener: NWListener? 15 | private var endpoint: NWEndpoint? 16 | private var connection: NWConnection? 17 | 18 | init(applicationService: String, didReceiveMessage: ( (Data) -> Void)? = nil, errorHandler: ( (Error) -> Void)? = nil, minimumIncompleteLength: Int = 1024, maximumLength: Int = 1024 * 512) { 19 | self.applicationService = applicationService 20 | self.didReceiveMessage = didReceiveMessage 21 | self.errorHandler = errorHandler 22 | self.minimumIncompleteLength = minimumIncompleteLength 23 | self.maximumLength = maximumLength 24 | } 25 | 26 | var isConnected: Bool { 27 | guard let connection else { return false } 28 | return connection.state == .ready 29 | } 30 | 31 | func connect(to endpoint: NWEndpoint) { 32 | self.endpoint = endpoint 33 | let connection = NWConnection(to: endpoint, using: .applicationService) 34 | setUpConnection(connection) 35 | } 36 | 37 | func disconnect() { 38 | connection?.cancel() 39 | connection = nil 40 | } 41 | 42 | func createListener() throws { 43 | listener = try NWListener(using: .applicationService) 44 | listener?.service = .init(applicationService: applicationService) 45 | 46 | listener?.stateUpdateHandler = { [weak self] state in 47 | guard let self else { return } 48 | switch state { 49 | case .failed(let error): 50 | errorHandler?(error) 51 | disconnect() 52 | default: 53 | break 54 | } 55 | self.objectWillChange.send() 56 | } 57 | 58 | listener?.newConnectionHandler = { connection in 59 | self.setUpConnection(connection) 60 | } 61 | 62 | listener?.start(queue: .main) 63 | } 64 | 65 | func send(_ string: String) { 66 | guard let data = string.data(using: .utf8) else { return } 67 | connection?.send(content: data, completion: .contentProcessed({ [weak self] error in 68 | guard let self else { return } 69 | if let error { 70 | errorHandler?(error) 71 | } 72 | })) 73 | } 74 | 75 | private func setUpConnection(_ connection: NWConnection) { 76 | self.connection = connection 77 | 78 | connection.stateUpdateHandler = { [weak self] state in 79 | guard let self else { return } 80 | switch state { 81 | case .failed(let error): 82 | errorHandler?(error) 83 | disconnect() 84 | default: 85 | break 86 | } 87 | self.objectWillChange.send() 88 | } 89 | 90 | receive() 91 | connection.start(queue: .main) 92 | } 93 | 94 | private func receive() { 95 | guard let connection else { return } 96 | connection.receive(minimumIncompleteLength: 1, maximumLength: 1024 * 1024) { [weak self] content, contentContext, isComplete, error in 97 | guard let self else { return } 98 | if let error { 99 | errorHandler?(error) 100 | } 101 | if let content { 102 | didReceiveMessage?(content) 103 | } 104 | receive() 105 | } 106 | } 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LocalDeviceManager 2 | Using `DeviceDiscoveryUI` to connect an Apple TV app to an iPhone, iPad, or Apple Watch. 3 | 4 | ## Limitations of DeviceDiscoveryUI 5 | - Only runs on Apple TV 4K currently (Apple TV HD is not supported) 6 | - 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) 7 | - 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](https://developer.apple.com/support/universal-purchase/)) 8 | - This will not work on either the tvOS or iOS simulators. You must use physical devices. 9 | 10 | ## Usage 11 | 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). 12 | 13 | ### Apple TV 14 | 15 | #### Step 1. 16 | Define supported devices with an [NSApplicationServices key](https://developer.apple.com/documentation/devicediscoveryui/connecting_a_tvos_app_to_other_devices_over_the_local_network#3976721) in your Info.plist 17 | 18 | #### Step 2. 19 | Instantiate the `LocalDeviceManager` using the application service key you defined in Step 1 (i.e. "remote" in this example): 20 | 21 | ``` 22 | @ObservedObject private var deviceManager = LocalDeviceManager(applicationService: "remote", didReceiveMessage: { data in 23 | guard let string = String(data: data, encoding: .utf8) else { return } 24 | NSLog("Message: \(string)") 25 | }, errorHandler: { error in 26 | NSLog("ERROR: \(error)") 27 | }) 28 | ``` 29 | 30 | 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. 31 | 32 | An error handler is also provided in case of failures. 33 | 34 | #### Step 3. 35 | Present the Device Picker UI. This demo uses SwiftUI but you can use [`DDDevicePickerViewController`](https://developer.apple.com/documentation/devicediscoveryui/dddevicepickerviewcontroller) in UIKit. 36 | 37 | ``` 38 | @State private var showDevicePicker = false 39 | 40 | var body: some View { 41 | VStack { 42 | if deviceManager.isConnected { 43 | Button("Send") { 44 | deviceManager.send("Hello from tvOS!") 45 | } 46 | 47 | Button("Disconnect") { 48 | deviceManager.disconnect() 49 | } 50 | } else { 51 | DevicePicker(.applicationService(name: "remote")) { endpoint in 52 | deviceManager.connect(to: endpoint) 53 | } label: { 54 | Text("Connect to a local device.") 55 | } fallback: { 56 | Text("Device browsing is not supported on this device") 57 | } parameters: { 58 | .applicationService 59 | } 60 | } 61 | } 62 | .padding() 63 | } 64 | ``` 65 | 66 | 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. 67 | 68 | ![Notifications from the tvOS Device Picker](https://bendodson.s3-eu-west-1.amazonaws.com/weblog/2023/DevicePicker-Notifications-iOS.jpg) 69 | 70 | 71 | ### iPhone / iPad / Apple Watch 72 | 73 | #### Step 1. 74 | Declare the device can listen for connections by using an [NSApplicationServices key](https://developer.apple.com/documentation/devicediscoveryui/connecting_a_tvos_app_to_other_devices_over_the_local_network#3986063) in your Info.plist 75 | 76 | #### Step 2. 77 | Instantiate the `LocalDeviceManager` using the application service key you defined in Step 1 (i.e. "remote" in this example): 78 | 79 | ``` 80 | @ObservedObject private var deviceManager = LocalDeviceManager(applicationService: "remote", didReceiveMessage: { data in 81 | guard let string = String(data: data, encoding: .utf8) else { return } 82 | NSLog("Message: \(string)") 83 | }, errorHandler: { error in 84 | NSLog("ERROR: \(error)") 85 | }) 86 | ``` 87 | 88 | This is identical to the tvOS implementation. 89 | 90 | #### Step 3. 91 | Create your UI and ensure that the `LocalDeviceManager` listener is created as soon as possible. 92 | 93 | ``` 94 | var body: some View { 95 | VStack { 96 | if deviceManager.isConnected { 97 | Text("Connected!") 98 | Button { 99 | deviceManager.send("Hello from iOS!") 100 | } label: { 101 | Text("Send") 102 | } 103 | Button { 104 | deviceManager.disconnect() 105 | } label: { 106 | Text("Disconnect") 107 | } 108 | } else { 109 | Text("Not Connected") 110 | } 111 | } 112 | .padding() 113 | .onAppear { 114 | try? deviceManager.createListener() 115 | } 116 | } 117 | ``` 118 | 119 | You can now send data from tvOS to your connected device and vice versa. 120 | 121 | ## Find Out More 122 | You can read [my blog post](https://bendodson.com/weblog/2023/05/10/connecting-a-tvos-app-to-ios-ipados-and-watchos-with-devicediscoveryui/) on this topic to learn more about DeviceDiscoveryUI in tvOS 16 and how I'm using this class in my own apps. 123 | -------------------------------------------------------------------------------- /Remote.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2BE6648A2A09130700E83244 /* RemoteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE664892A09130700E83244 /* RemoteApp.swift */; }; 11 | 2BE6648C2A09130700E83244 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE6648B2A09130700E83244 /* ContentView.swift */; }; 12 | 2BE6648E2A09130800E83244 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BE6648D2A09130800E83244 /* Assets.xcassets */; }; 13 | 2BE664912A09130800E83244 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BE664902A09130800E83244 /* Preview Assets.xcassets */; }; 14 | 2BE664992A09147B00E83244 /* LocalDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE664982A09147B00E83244 /* LocalDeviceManager.swift */; }; 15 | 2BE664A12A0917F300E83244 /* RemotePhoneApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE664A02A0917F300E83244 /* RemotePhoneApp.swift */; }; 16 | 2BE664A32A0917F300E83244 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE664A22A0917F300E83244 /* ContentView.swift */; }; 17 | 2BE664A52A0917F500E83244 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BE664A42A0917F500E83244 /* Assets.xcassets */; }; 18 | 2BE664A82A0917F500E83244 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BE664A72A0917F500E83244 /* Preview Assets.xcassets */; }; 19 | 2BE664AC2A09182600E83244 /* LocalDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE664982A09147B00E83244 /* LocalDeviceManager.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 2BAB82F02A091ACC0038B2DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 24 | 2BE664862A09130700E83244 /* RemoteTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RemoteTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 2BE664892A09130700E83244 /* RemoteApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteApp.swift; sourceTree = ""; }; 26 | 2BE6648B2A09130700E83244 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 27 | 2BE6648D2A09130800E83244 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 2BE664902A09130800E83244 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 29 | 2BE664972A09133500E83244 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 30 | 2BE664982A09147B00E83244 /* LocalDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalDeviceManager.swift; sourceTree = ""; }; 31 | 2BE6649E2A0917F300E83244 /* RemotePhone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RemotePhone.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 2BE664A02A0917F300E83244 /* RemotePhoneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePhoneApp.swift; sourceTree = ""; }; 33 | 2BE664A22A0917F300E83244 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 34 | 2BE664A42A0917F500E83244 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35 | 2BE664A72A0917F500E83244 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 36 | /* End PBXFileReference section */ 37 | 38 | /* Begin PBXFrameworksBuildPhase section */ 39 | 2BE664832A09130700E83244 /* Frameworks */ = { 40 | isa = PBXFrameworksBuildPhase; 41 | buildActionMask = 2147483647; 42 | files = ( 43 | ); 44 | runOnlyForDeploymentPostprocessing = 0; 45 | }; 46 | 2BE6649B2A0917F300E83244 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 2BE6647D2A09130700E83244 = { 57 | isa = PBXGroup; 58 | children = ( 59 | 2BE664982A09147B00E83244 /* LocalDeviceManager.swift */, 60 | 2BE664882A09130700E83244 /* RemoteTV */, 61 | 2BE6649F2A0917F300E83244 /* RemotePhone */, 62 | 2BE664872A09130700E83244 /* Products */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | 2BE664872A09130700E83244 /* Products */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 2BE664862A09130700E83244 /* RemoteTV.app */, 70 | 2BE6649E2A0917F300E83244 /* RemotePhone.app */, 71 | ); 72 | name = Products; 73 | sourceTree = ""; 74 | }; 75 | 2BE664882A09130700E83244 /* RemoteTV */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 2BE664972A09133500E83244 /* Info.plist */, 79 | 2BE664892A09130700E83244 /* RemoteApp.swift */, 80 | 2BE6648B2A09130700E83244 /* ContentView.swift */, 81 | 2BE6648D2A09130800E83244 /* Assets.xcassets */, 82 | 2BE6648F2A09130800E83244 /* Preview Content */, 83 | ); 84 | path = RemoteTV; 85 | sourceTree = ""; 86 | }; 87 | 2BE6648F2A09130800E83244 /* Preview Content */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 2BE664902A09130800E83244 /* Preview Assets.xcassets */, 91 | ); 92 | path = "Preview Content"; 93 | sourceTree = ""; 94 | }; 95 | 2BE6649F2A0917F300E83244 /* RemotePhone */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 2BAB82F02A091ACC0038B2DD /* Info.plist */, 99 | 2BE664A02A0917F300E83244 /* RemotePhoneApp.swift */, 100 | 2BE664A22A0917F300E83244 /* ContentView.swift */, 101 | 2BE664A42A0917F500E83244 /* Assets.xcassets */, 102 | 2BE664A62A0917F500E83244 /* Preview Content */, 103 | ); 104 | path = RemotePhone; 105 | sourceTree = ""; 106 | }; 107 | 2BE664A62A0917F500E83244 /* Preview Content */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 2BE664A72A0917F500E83244 /* Preview Assets.xcassets */, 111 | ); 112 | path = "Preview Content"; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | 2BE664852A09130700E83244 /* RemoteTV */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = 2BE664942A09130800E83244 /* Build configuration list for PBXNativeTarget "RemoteTV" */; 121 | buildPhases = ( 122 | 2BE664822A09130700E83244 /* Sources */, 123 | 2BE664832A09130700E83244 /* Frameworks */, 124 | 2BE664842A09130700E83244 /* Resources */, 125 | ); 126 | buildRules = ( 127 | ); 128 | dependencies = ( 129 | ); 130 | name = RemoteTV; 131 | productName = Remote; 132 | productReference = 2BE664862A09130700E83244 /* RemoteTV.app */; 133 | productType = "com.apple.product-type.application"; 134 | }; 135 | 2BE6649D2A0917F300E83244 /* RemotePhone */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = 2BE664A92A0917F500E83244 /* Build configuration list for PBXNativeTarget "RemotePhone" */; 138 | buildPhases = ( 139 | 2BE6649A2A0917F300E83244 /* Sources */, 140 | 2BE6649B2A0917F300E83244 /* Frameworks */, 141 | 2BE6649C2A0917F300E83244 /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = RemotePhone; 148 | productName = RemotePhone; 149 | productReference = 2BE6649E2A0917F300E83244 /* RemotePhone.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 2BE6647E2A09130700E83244 /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | BuildIndependentTargetsInParallel = 1; 159 | LastSwiftUpdateCheck = 1430; 160 | LastUpgradeCheck = 1430; 161 | TargetAttributes = { 162 | 2BE664852A09130700E83244 = { 163 | CreatedOnToolsVersion = 14.3; 164 | }; 165 | 2BE6649D2A0917F300E83244 = { 166 | CreatedOnToolsVersion = 14.3; 167 | }; 168 | }; 169 | }; 170 | buildConfigurationList = 2BE664812A09130700E83244 /* Build configuration list for PBXProject "Remote" */; 171 | compatibilityVersion = "Xcode 14.0"; 172 | developmentRegion = en; 173 | hasScannedForEncodings = 0; 174 | knownRegions = ( 175 | en, 176 | Base, 177 | ); 178 | mainGroup = 2BE6647D2A09130700E83244; 179 | productRefGroup = 2BE664872A09130700E83244 /* Products */; 180 | projectDirPath = ""; 181 | projectRoot = ""; 182 | targets = ( 183 | 2BE664852A09130700E83244 /* RemoteTV */, 184 | 2BE6649D2A0917F300E83244 /* RemotePhone */, 185 | ); 186 | }; 187 | /* End PBXProject section */ 188 | 189 | /* Begin PBXResourcesBuildPhase section */ 190 | 2BE664842A09130700E83244 /* Resources */ = { 191 | isa = PBXResourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | 2BE664912A09130800E83244 /* Preview Assets.xcassets in Resources */, 195 | 2BE6648E2A09130800E83244 /* Assets.xcassets in Resources */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | 2BE6649C2A0917F300E83244 /* Resources */ = { 200 | isa = PBXResourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | 2BE664A82A0917F500E83244 /* Preview Assets.xcassets in Resources */, 204 | 2BE664A52A0917F500E83244 /* Assets.xcassets in Resources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXResourcesBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 2BE664822A09130700E83244 /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 2BE6648C2A09130700E83244 /* ContentView.swift in Sources */, 216 | 2BE6648A2A09130700E83244 /* RemoteApp.swift in Sources */, 217 | 2BE664992A09147B00E83244 /* LocalDeviceManager.swift in Sources */, 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | 2BE6649A2A0917F300E83244 /* Sources */ = { 222 | isa = PBXSourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 2BE664A32A0917F300E83244 /* ContentView.swift in Sources */, 226 | 2BE664A12A0917F300E83244 /* RemotePhoneApp.swift in Sources */, 227 | 2BE664AC2A09182600E83244 /* LocalDeviceManager.swift in Sources */, 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | }; 231 | /* End PBXSourcesBuildPhase section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 2BE664922A09130800E83244 /* Debug */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 240 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_ENABLE_OBJC_WEAK = YES; 244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_COMMA = YES; 247 | CLANG_WARN_CONSTANT_CONVERSION = YES; 248 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 250 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 251 | CLANG_WARN_EMPTY_BODY = YES; 252 | CLANG_WARN_ENUM_CONVERSION = YES; 253 | CLANG_WARN_INFINITE_RECURSION = YES; 254 | CLANG_WARN_INT_CONVERSION = YES; 255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 260 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 261 | CLANG_WARN_STRICT_PROTOTYPES = YES; 262 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 263 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 264 | CLANG_WARN_UNREACHABLE_CODE = YES; 265 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 266 | COPY_PHASE_STRIP = NO; 267 | DEBUG_INFORMATION_FORMAT = dwarf; 268 | ENABLE_STRICT_OBJC_MSGSEND = YES; 269 | ENABLE_TESTABILITY = YES; 270 | GCC_C_LANGUAGE_STANDARD = gnu11; 271 | GCC_DYNAMIC_NO_PIC = NO; 272 | GCC_NO_COMMON_BLOCKS = YES; 273 | GCC_OPTIMIZATION_LEVEL = 0; 274 | GCC_PREPROCESSOR_DEFINITIONS = ( 275 | "DEBUG=1", 276 | "$(inherited)", 277 | ); 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 285 | MTL_FAST_MATH = YES; 286 | ONLY_ACTIVE_ARCH = YES; 287 | SDKROOT = appletvos; 288 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 289 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 290 | TVOS_DEPLOYMENT_TARGET = 16.4; 291 | }; 292 | name = Debug; 293 | }; 294 | 2BE664932A09130800E83244 /* Release */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | ALWAYS_SEARCH_USER_PATHS = NO; 298 | CLANG_ANALYZER_NONNULL = YES; 299 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 300 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 301 | CLANG_ENABLE_MODULES = YES; 302 | CLANG_ENABLE_OBJC_ARC = YES; 303 | CLANG_ENABLE_OBJC_WEAK = YES; 304 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 305 | CLANG_WARN_BOOL_CONVERSION = YES; 306 | CLANG_WARN_COMMA = YES; 307 | CLANG_WARN_CONSTANT_CONVERSION = YES; 308 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 309 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 310 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 311 | CLANG_WARN_EMPTY_BODY = YES; 312 | CLANG_WARN_ENUM_CONVERSION = YES; 313 | CLANG_WARN_INFINITE_RECURSION = YES; 314 | CLANG_WARN_INT_CONVERSION = YES; 315 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 316 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 317 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 318 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 319 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 320 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 321 | CLANG_WARN_STRICT_PROTOTYPES = YES; 322 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 323 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 324 | CLANG_WARN_UNREACHABLE_CODE = YES; 325 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 326 | COPY_PHASE_STRIP = NO; 327 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 328 | ENABLE_NS_ASSERTIONS = NO; 329 | ENABLE_STRICT_OBJC_MSGSEND = YES; 330 | GCC_C_LANGUAGE_STANDARD = gnu11; 331 | GCC_NO_COMMON_BLOCKS = YES; 332 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 333 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 334 | GCC_WARN_UNDECLARED_SELECTOR = YES; 335 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 336 | GCC_WARN_UNUSED_FUNCTION = YES; 337 | GCC_WARN_UNUSED_VARIABLE = YES; 338 | MTL_ENABLE_DEBUG_INFO = NO; 339 | MTL_FAST_MATH = YES; 340 | SDKROOT = appletvos; 341 | SWIFT_COMPILATION_MODE = wholemodule; 342 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 343 | TVOS_DEPLOYMENT_TARGET = 16.4; 344 | VALIDATE_PRODUCT = YES; 345 | }; 346 | name = Release; 347 | }; 348 | 2BE664952A09130800E83244 /* Debug */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 352 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 353 | CODE_SIGN_STYLE = Automatic; 354 | CURRENT_PROJECT_VERSION = 1; 355 | DEVELOPMENT_ASSET_PATHS = "\"RemoteTV/Preview Content\""; 356 | DEVELOPMENT_TEAM = 7V89NHL866; 357 | ENABLE_PREVIEWS = YES; 358 | GENERATE_INFOPLIST_FILE = YES; 359 | INFOPLIST_FILE = RemoteTV/Info.plist; 360 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 361 | INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; 362 | LD_RUNPATH_SEARCH_PATHS = ( 363 | "$(inherited)", 364 | "@executable_path/Frameworks", 365 | ); 366 | MARKETING_VERSION = 1.0; 367 | PRODUCT_BUNDLE_IDENTIFIER = io.dodoapps.remote; 368 | PRODUCT_NAME = "$(TARGET_NAME)"; 369 | SWIFT_EMIT_LOC_STRINGS = YES; 370 | SWIFT_VERSION = 5.0; 371 | TARGETED_DEVICE_FAMILY = 3; 372 | }; 373 | name = Debug; 374 | }; 375 | 2BE664962A09130800E83244 /* Release */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; 379 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 380 | CODE_SIGN_STYLE = Automatic; 381 | CURRENT_PROJECT_VERSION = 1; 382 | DEVELOPMENT_ASSET_PATHS = "\"RemoteTV/Preview Content\""; 383 | DEVELOPMENT_TEAM = 7V89NHL866; 384 | ENABLE_PREVIEWS = YES; 385 | GENERATE_INFOPLIST_FILE = YES; 386 | INFOPLIST_FILE = RemoteTV/Info.plist; 387 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 388 | INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; 389 | LD_RUNPATH_SEARCH_PATHS = ( 390 | "$(inherited)", 391 | "@executable_path/Frameworks", 392 | ); 393 | MARKETING_VERSION = 1.0; 394 | PRODUCT_BUNDLE_IDENTIFIER = io.dodoapps.remote; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | SWIFT_EMIT_LOC_STRINGS = YES; 397 | SWIFT_VERSION = 5.0; 398 | TARGETED_DEVICE_FAMILY = 3; 399 | }; 400 | name = Release; 401 | }; 402 | 2BE664AA2A0917F500E83244 /* Debug */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 406 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 407 | CODE_SIGN_STYLE = Automatic; 408 | CURRENT_PROJECT_VERSION = 1; 409 | DEVELOPMENT_ASSET_PATHS = "\"RemotePhone/Preview Content\""; 410 | DEVELOPMENT_TEAM = 7V89NHL866; 411 | ENABLE_PREVIEWS = YES; 412 | GENERATE_INFOPLIST_FILE = YES; 413 | INFOPLIST_FILE = RemotePhone/Info.plist; 414 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 415 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 416 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 417 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 418 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 419 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 420 | LD_RUNPATH_SEARCH_PATHS = ( 421 | "$(inherited)", 422 | "@executable_path/Frameworks", 423 | ); 424 | MARKETING_VERSION = 1.0; 425 | PRODUCT_BUNDLE_IDENTIFIER = io.dodoapps.remote; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | SDKROOT = iphoneos; 428 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 429 | SUPPORTS_MACCATALYST = NO; 430 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 431 | SWIFT_EMIT_LOC_STRINGS = YES; 432 | SWIFT_VERSION = 5.0; 433 | TARGETED_DEVICE_FAMILY = 1; 434 | }; 435 | name = Debug; 436 | }; 437 | 2BE664AB2A0917F500E83244 /* Release */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 441 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 442 | CODE_SIGN_STYLE = Automatic; 443 | CURRENT_PROJECT_VERSION = 1; 444 | DEVELOPMENT_ASSET_PATHS = "\"RemotePhone/Preview Content\""; 445 | DEVELOPMENT_TEAM = 7V89NHL866; 446 | ENABLE_PREVIEWS = YES; 447 | GENERATE_INFOPLIST_FILE = YES; 448 | INFOPLIST_FILE = RemotePhone/Info.plist; 449 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 450 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 451 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 452 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 453 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 454 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 455 | LD_RUNPATH_SEARCH_PATHS = ( 456 | "$(inherited)", 457 | "@executable_path/Frameworks", 458 | ); 459 | MARKETING_VERSION = 1.0; 460 | PRODUCT_BUNDLE_IDENTIFIER = io.dodoapps.remote; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | SDKROOT = iphoneos; 463 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 464 | SUPPORTS_MACCATALYST = NO; 465 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 466 | SWIFT_EMIT_LOC_STRINGS = YES; 467 | SWIFT_VERSION = 5.0; 468 | TARGETED_DEVICE_FAMILY = 1; 469 | }; 470 | name = Release; 471 | }; 472 | /* End XCBuildConfiguration section */ 473 | 474 | /* Begin XCConfigurationList section */ 475 | 2BE664812A09130700E83244 /* Build configuration list for PBXProject "Remote" */ = { 476 | isa = XCConfigurationList; 477 | buildConfigurations = ( 478 | 2BE664922A09130800E83244 /* Debug */, 479 | 2BE664932A09130800E83244 /* Release */, 480 | ); 481 | defaultConfigurationIsVisible = 0; 482 | defaultConfigurationName = Release; 483 | }; 484 | 2BE664942A09130800E83244 /* Build configuration list for PBXNativeTarget "RemoteTV" */ = { 485 | isa = XCConfigurationList; 486 | buildConfigurations = ( 487 | 2BE664952A09130800E83244 /* Debug */, 488 | 2BE664962A09130800E83244 /* Release */, 489 | ); 490 | defaultConfigurationIsVisible = 0; 491 | defaultConfigurationName = Release; 492 | }; 493 | 2BE664A92A0917F500E83244 /* Build configuration list for PBXNativeTarget "RemotePhone" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | 2BE664AA2A0917F500E83244 /* Debug */, 497 | 2BE664AB2A0917F500E83244 /* Release */, 498 | ); 499 | defaultConfigurationIsVisible = 0; 500 | defaultConfigurationName = Release; 501 | }; 502 | /* End XCConfigurationList section */ 503 | }; 504 | rootObject = 2BE6647E2A09130700E83244 /* Project object */; 505 | } 506 | -------------------------------------------------------------------------------- /Remote.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Remote.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Remote.xcodeproj/xcshareddata/xcschemes/RemoteTV.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Remote.xcodeproj/xcuserdata/bendodson.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Remote.xcodeproj/xcuserdata/bendodson.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RemotePhone.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | RemoteTV.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 2BE664852A09130700E83244 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /RemotePhone/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RemotePhone/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RemotePhone/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemotePhone/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Developed by Ben Dodson (ben@bendodson.com) 2 | 3 | import SwiftUI 4 | 5 | struct ContentView: View { 6 | 7 | @ObservedObject private var deviceManager = LocalDeviceManager(applicationService: "remote", didReceiveMessage: { data in 8 | guard let string = String(data: data, encoding: .utf8) else { return } 9 | NSLog("Message: \(string)") 10 | }, errorHandler: { error in 11 | NSLog("ERROR: \(error)") 12 | }) 13 | 14 | var body: some View { 15 | VStack { 16 | if deviceManager.isConnected { 17 | Text("Connected!") 18 | Button { 19 | deviceManager.send("Hello from iOS!") 20 | } label: { 21 | Text("Send") 22 | } 23 | Button { 24 | deviceManager.disconnect() 25 | } label: { 26 | Text("Disconnect") 27 | } 28 | 29 | } else { 30 | Text("Not Connected") 31 | } 32 | } 33 | .padding() 34 | .onAppear { 35 | try? deviceManager.createListener() 36 | } 37 | } 38 | } 39 | 40 | struct ContentView_Previews: PreviewProvider { 41 | static var previews: some View { 42 | ContentView() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /RemotePhone/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSApplicationServices 6 | 7 | Advertises 8 | 9 | 10 | NSApplicationServiceIdentifier 11 | remote 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /RemotePhone/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemotePhone/RemotePhoneApp.swift: -------------------------------------------------------------------------------- 1 | // Developed by Ben Dodson (ben@bendodson.com) 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct RemotePhoneApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | ContentView() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RemoteTV/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Developed by Ben Dodson (ben@bendodson.com) 2 | 3 | import SwiftUI 4 | import DeviceDiscoveryUI 5 | 6 | struct ContentView: View { 7 | 8 | 9 | 10 | @ObservedObject private var deviceManager = LocalDeviceManager(applicationService: "remote", didReceiveMessage: { data in 11 | guard let string = String(data: data, encoding: .utf8) else { return } 12 | NSLog("Message: \(string)") 13 | }, errorHandler: { error in 14 | NSLog("ERROR: \(error)") 15 | }) 16 | 17 | @State private var showDevicePicker = false 18 | 19 | var body: some View { 20 | VStack { 21 | 22 | if deviceManager.isConnected { 23 | Button("Send") { 24 | deviceManager.send("Hello from tvOS!") 25 | } 26 | 27 | Button("Disconnect") { 28 | deviceManager.disconnect() 29 | } 30 | } else { 31 | DevicePicker(.applicationService(name: "remote")) { endpoint in 32 | deviceManager.connect(to: endpoint) 33 | } label: { 34 | Text("Connect to a local device.") 35 | } fallback: { 36 | Text("Device browsing is not supported on this device") 37 | } parameters: { 38 | .applicationService 39 | } 40 | } 41 | 42 | } 43 | .padding() 44 | 45 | } 46 | } 47 | 48 | struct ContentView_Previews: PreviewProvider { 49 | static var previews: some View { 50 | ContentView() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RemoteTV/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSApplicationServices 6 | 7 | Browses 8 | 9 | 10 | NSApplicationServiceIdentifier 11 | remote 12 | NSApplicationServicePlatformSupport 13 | 14 | iOS 15 | 16 | NSApplicationServiceUsageDescription 17 | This app can be controlled from iOS 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /RemoteTV/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RemoteTV/RemoteApp.swift: -------------------------------------------------------------------------------- 1 | // Developed by Ben Dodson (ben@bendodson.com) 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct RemoteApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | ContentView() 10 | } 11 | } 12 | } 13 | --------------------------------------------------------------------------------