Displaying SwiftUI content on an external screen in iOS
This brief article will dive into how you can use SwiftUI, UIKit and scene delegates to present content on an external display with iOS.
As a reader of our developer blog, I believe you might be familiar with development for big screens with macOS. Today, we'll talk about a different type of development for big screens — we'll talk about using external displays with iOS.
A brief history
Mirroring the content of an iOS device to an external display has been possible since 2010 with the introduction of AirPlay. The following year, it became possible to use an adapter to directly connect an iPhone to an external display.
Basic mirroring isn't that big of a deal: it only shows whatever your phone's main screen is already showing. Some specific apps though, such as Photos, use an external display connection to show alternative, non-interactive content, such as full screen pictures and videos.
This has always been possible, but has gotten much easier in 2019 with iOS 13 with the introduction of the UIScene
API. And in 2023, with the very awaited switch of the iPhone to USB-C, it became even easier to connect an iPhone to a monitor or TV. Let's see how we can leverage that today!
Start by creating a new SwiftUI project in Xcode and naming it as you wish. Then, we'll need to make some changes to the project's configuration.
Editing your Info.plist
First, you need to properly configure your Info.plist
to support multiple screens. First, under the "Application Scene Manifest" (UIApplicationSceneManifest
) key, set "Enable Multiple Windows" (UIApplicationSupportsMultipleScenes
) to YES
.
Then, under the "Scene Configuration" key, add a new item with the key "External Display Session Role Non-Interactive" (UIWindowSceneSessionRoleExternalDisplayNonInteractive), set its "Configuration Name" (UISceneConfigurationName
) to "External Display" and its "Delegate Class Name" (UISceneDelegateClassName
) to $(PRODUCT_MODULE_NAME).SceneDelegate
.
This will inform the system of how your app will handle an external display. This is what your Info.plist should look like in the graphical editor after these changes:
Fun fact: this is the same configuration property that allows your app to support multiple windows on iPadOS, or immersive spaces and on visionOS.
Creating a new scene delegate
To create a scene delegate for a SwiftUI app, first we need to specify a custom app delegate. Let's first create a new AppDelegate.swift
file:
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
return true
}
}
Then, on our @main
entry point, we need to specify that we're using a custom app delegate:
@main
struct ExternalDisplayApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Now, we can create a new SceneDelegate.swift
file, and add the code to handle the external display:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
if session.role == .windowExternalDisplayNonInteractive {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ExternalDisplayView()) // Here we specify the view we want to display on the external screen
self.window = window
window.makeKeyAndVisible()
}
}
}
Notice how we're setting the rootViewController
of the window to a UIHostingController
with the view we want to display on the external screen. In this case, we're using a view called ExternalView
. Let's create that view as well:
struct ExternalDisplayView: View {
var body: some View {
Text("Hello, world!")
.font(.system(size: 96, weight: .bold))
}
}
Finally, add the following method to your AppDelegate
:
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let sceneConfiguration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
sceneConfiguration.delegateClass = SceneDelegate.self // Here we specify the scene delegate we just created
return sceneConfiguration
}
And that's it! You can check that it works by connecting your iPhone to an external display and running the app. You should see the text "Hello, world!" displayed on the external screen.
If you can't connect an iPhone to an external display, you can create one in the Simulator, just go to the "I/O" menu, select "External Displays" and choose your desired resolution.
Interfacing with the external display
Now comes the fun part: implementing something useful for the external diplay. You can do pretty much anything you want, but remember: this view will not be interactive. In this example, we'll implement a simple counter that will be shown on the external display.
Let's start by writing a DataModel to hold the counter value:
class DataModel: ObservableObject {
@Published var counter = 0
func increment() {
counter += 1
}
static var shared = DataModel() // Let's make it a singleton
}
For the sake of simplicity, we've made the DataModel
a singleton.
Now, let's modify our ContentView
to display a button to increment it:
import SwiftUI
struct ContentView: View {
@ObservedObject var dataModel = DataModel.shared
var body: some View {
Button(action: dataModel.increment) {
Text("Increment")
}
}
}
Finally, let's modify our ExternalDisplayView
to display the counter:
import SwiftUI
struct ExternalView: View {
@ObservedObject var dataModel = DataModel.shared
var body: some View {
Text("\(dataModel.counter)")
.font(.system(size: 96, weight: .bold))
.contentTransition(.numericText()) // Let's add a transition to the text
.animation(.default, value: dataModel.counter) // And specify an animation
}
}
We've also added a numericText
transition and animation to the text, so that it animates when the counter changes.
And that's it! You can now run the app and see the counter incrementing on the external display.
If you can, you can also try the app outside of the iOS Simulator, by connecting your iPhone to an external display. Now that you've seen it work, think of all the ways you can use this feature to enhance the user experience of your app!