Airplay - External monitor support with SwiftUI in iOS 14

Viewed 219

Xcode 12 no longer generates projects with an AppDelegate, and so all the guides out there on streaming your app to an external display no longer work.

Now we have WindowGroup to play with.

I can't figure out how to get Airplay working with this new approach.

I've looked at Implementing external monitor support in SwiftUI but that uses delegate methods that aren't available.

Is this even possible at the moment?

1 Answers

Turns out you need to listen for UIScreen.didConnectNotification and then make changes to UIWindow.

This is how I solved it.

You need to listen for the connect/disconnect notifications. These are triggered when a user does the whole screen share thing.

In have the following listeners on my root View inside WindowGroup in the main App file:

.onReceive(screenDidConnectPublisher, perform: screenDidConnect)
.onReceive(screenDidDisconnectPublisher, perform: screenDidDisconnect)

Here are the publishers:

private var screenDidConnectPublisher: AnyPublisher<UIScreen, Never> {
  NotificationCenter.default
    .publisher(for: UIScreen.didConnectNotification)
    .compactMap { $0.object as? UIScreen }
    .receive(on: RunLoop.main)
    .eraseToAnyPublisher()
}
    
private var screenDidDisconnectPublisher: AnyPublisher<UIScreen, Never> {
  NotificationCenter.default
    .publisher(for: UIScreen.didDisconnectNotification)
    .compactMap { $0.object as? UIScreen }
    .receive(on: RunLoop.main)
    .eraseToAnyPublisher()
}

The final parts are the functions that you run when the notification is received:

private func screenDidConnect(_ screen: UIScreen) {
   let window = UIWindow(frame: screen.bounds)

   window.windowScene = UIApplication.shared.connectedScenes
            .first { ($0 as? UIWindowScene)?.screen == screen }
            as? UIWindowScene

   let view = ExternalView().environmentObject(externalDisplayContent)
   let controller = UIHostingController(rootView: view)
   window.rootViewController = controller
   window.isHidden = false
   additionalWindows.append(window)
        
   externalDisplayContent.isShowingOnExternalDisplay = true
}
    
private func screenDidDisconnect(_ screen: UIScreen) {
   additionalWindows.removeAll { $0.screen == screen }
   externalDisplayContent.isShowingOnExternalDisplay = false
}

externalDisplayContent is just a custom struct I created that passes along the information I want to display on the ExternalView, which is just a regular SwiftUI view.

You can see that the content gets passed through as an environment object, but it doesn't have to. You can use an initialiser on the view instead if you prefer.

I think you may also get away with passing through bindings.

Related