<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Amol Srivastava]]></title><description><![CDATA[Amol Srivastava]]></description><link>https://amolsrivastava.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Amol Srivastava</title><link>https://amolsrivastava.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 16:42:28 GMT</lastBuildDate><atom:link href="https://amolsrivastava.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Detecting and Targeting AirPlay Displays in SwiftUI: A Practical Guide]]></title><description><![CDATA[Most SwiftUI developers treat AirPlay as a media-streaming feature bolted onto AVPlayer. It's also a second screen you can render arbitrary SwiftUI content to — and almost nobody uses it that way. Aft]]></description><link>https://amolsrivastava.hashnode.dev/detecting-and-targeting-airplay-displays-in-swiftui-a-practical-guide</link><guid isPermaLink="true">https://amolsrivastava.hashnode.dev/detecting-and-targeting-airplay-displays-in-swiftui-a-practical-guide</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Amol Srivastava]]></dc:creator><pubDate>Sun, 23 Aug 2026 10:00:05 GMT</pubDate><content:encoded><![CDATA[<p>Most SwiftUI developers treat AirPlay as a media-streaming feature bolted onto <code>AVPlayer</code>. It's also a second screen you can render arbitrary SwiftUI content to — and almost nobody uses it that way. After building a dozen-plus apps that cast a live SwiftUI board to a TV while the phone stays a private controller, here's the actual mechanism, not just the AVKit demo.</p>
<h2>The core idea</h2>
<p>An AirPlay-connected Apple TV shows up to your app as an external <code>UIScreen</code>. Once you notice one connected, you create a second <code>UIWindow</code> scoped to that screen and hand it its own SwiftUI view hierarchy. Your original window keeps running as the controller UI. Two completely independent view trees, sharing only whatever state you choose to pipe between them.</p>
<h2>Detecting the external display</h2>
<pre><code class="language-swift">import UIKit
import Combine

final class ExternalDisplayObserver: ObservableObject {
    @Published var externalScreen: UIScreen?

    private var cancellables = Set&lt;AnyCancellable&gt;()

    init() {
        externalScreen = UIScreen.screens.first { $0 != UIScreen.main }

        NotificationCenter.default.publisher(for: UIScreen.didConnectNotification)
            .compactMap { $0.object as? UIScreen }
            .sink { [weak self] screen in self?.externalScreen = screen }
            .store(in: &amp;cancellables)

        NotificationCenter.default.publisher(for: UIScreen.didDisconnectNotification)
            .sink { [weak self] _ in self?.externalScreen = nil }
            .store(in: &amp;cancellables)
    }
}
</code></pre>
<p>That's the whole detection layer. No AVKit, no route picker parsing — just watching for a screen that isn't the main one.</p>
<h2>Standing up the second window</h2>
<p>This is the part that trips people up, because <code>UIWindowScene</code> in a SwiftUI-lifecycle app doesn't hand you a second scene automatically just because a screen connected. You create the window yourself and attach it to the connected screen directly:</p>
<pre><code class="language-swift">final class ExternalWindowController {
    private var externalWindow: UIWindow?

    func present&lt;Content: View&gt;(_ content: Content, on screen: UIScreen) {
        let window = UIWindow(frame: screen.bounds)
        window.screen = screen
        window.rootViewController = UIHostingController(rootView: content)
        window.isHidden = false
        externalWindow = window
    }

    func dismiss() {
        externalWindow?.isHidden = true
        externalWindow = nil
    }
}
</code></pre>
<p>Wire it to the observer above: when <code>externalScreen</code> goes non-nil, call <code>present</code>; when it goes nil, call <code>dismiss</code>. The TV-facing view and the phone-facing view can now be two entirely different SwiftUI hierarchies driven by the same <code>@StateObject</code> game/session model, updating independently.</p>
<h2>Things that actually matter once you ship this</h2>
<p><strong>Test on real hardware.</strong> The Simulator's AirPlay support is unreliable for external-screen connect/disconnect notifications. You will chase phantom bugs if you trust it. Keep a real Apple TV on your desk.</p>
<p><strong>Design for viewing distance, not touch distance.</strong> The TV window is read from six to ten feet away by multiple people at once. Font sizes and contrast that look fine on a phone preview look thin and grey on a television. Oversize everything on the external view relative to your instinct.</p>
<p><strong>Handle disconnection as a first-class state, not an edge case.</strong> People walk out of AirPlay range, TVs go to sleep, someone else's AirPlay request steals the route. Your controller view needs a real "TV disconnected" state that pauses or gracefully degrades the session, not a force-unwrap crash.</p>
<p><strong>Don't put game logic in the window controller.</strong> Keep <code>ExternalWindowController</code> dumb — it only knows how to mount and unmount a view on a screen. All state belongs in an observable session object that both windows read from, so reconnecting the TV mid-session just reattaches the view instead of losing state.</p>
<p>That's the entire pattern. It scales from a simple scoreboard to a full dual-screen party game, and once it clicks, you start noticing how much SwiftUI UI could live on a second screen that almost nobody builds for.</p>
]]></content:encoded></item></channel></rss>