Using UIScenes in iOS 13, how do I AirPlay Mirror a screen (seems to default to external display)

1.8k views Asked by At

If I compile onto an iOS 12 device (doesn't use UIScene) and AirPlay Mirror to my Apple TV the app is mirrored as expected to the TV.

On an iOS 13 device, it seems to treat it as an external display where it's formatted to fit the screen (but I have no way to control it).

I'd prefer the old functionality of just mirroring it.

How do I accomplish mirroring on iOS 13? I'm digging around in the docs for:

application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration

And in the UISceneConfiguration there's a role property (it has UISceneSession.Role.windowExternalDisplay when I try to AirPlay Mirror) but it doesn't seem to have any value like UISceneSession.Role.windowMirror.

4

There are 4 answers

3
rmaddy On BEST ANSWER

I've been playing around with mirroring and external displays and various possibilities exist with just the right combination of code/settings but certain functionality doesn't seem possible.

Under iOS 13 (with an app built with a Base SDK of iOS 13), you can get your app to be mirrored on an external display. But making this work prevents your app from showing different content on an external display. Basically your app only mirrors or it only shows a unique scene for an external display.

If you wish to only have your app be mirrored, then ensure the following:

  1. Remove the application(_:configurationForConnecting:options:) from your App Delegate.
  2. In the Info.plist, make sure there is no entry for the "External Display Session Role" under the "Scene Configuration" section of the "Application Scene Manifest".

If neither of those two things are part of your app then your app will simple mirror to any external screen when you activate Screen Mirroring on the iOS device.

0
Jonny On

I found that with Objective-C implementation, you can achieve the screen mirroring behavior by returning nil in application:configurationForConnectingSceneSession:options:.

- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
    if (connectingSceneSession.role == UIWindowSceneSessionRoleExternalDisplay) {
        return nil;
    }
    UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Main" sessionRole:connectingSceneSession.role];
    configuration.storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    configuration.delegateClass = [SceneDelegate class];
    configuration.sceneClass = [UIWindowScene class];
    return configuration;
}

Be aware that this is not a documented way and may break in the future.

Edited: In Swift, you can achieve this via method swizzling:

@UIApplicationMain
class AppDelegate : UIResponder, UIApplicationDelegate {

    override init() {
        _ = AppDelegate.performSceneConfigurationSwizzle
        super.init()
    }

    private static let performSceneConfigurationSwizzle: Void = {
        method_exchangeImplementations(
            class_getInstanceMethod(AppDelegate.self, #selector(AppDelegate.application(_:configurationForConnecting:options:)))!,
            class_getInstanceMethod(AppDelegate.self, #selector(AppDelegate.swizzle_application(_:configurationForConnecting:options:)))!
        )
    }()

    @objc func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        fatalError("Should never reach.")
    }

    @objc private func swizzle_application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration? {
        if connectingSceneSession.role == .windowExternalDisplay {
            return nil
        }
        // build scene configuration as usual…
    }
}
0
christianselig On

Instead of implementing the AppDelegate scene configuration method in iOS 13:

@available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    configuration.delegateClass = SceneDelegate.self
    return configuration
}

I instead switched to using the Info.plist variant (and removed the above code) where you effectively specify all the above in your Info.plist instead. (For an up to date version of what's expected in the Info.plist file, simply create a New Project in Xcode and copy the contents from the new Info.plist file for the Application Scene Manifest key).

It now works perfectly and AirPlay Mirror mirrors as expected. I did try changing the role to windowApplication as iOS seemingly does with the Info.plist variant but it still doesn't work.

0
bclymer On

Just ran into this issue myself. My solution actually came from within my UIWindowSceneDelegate class.

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        // External displays should not get assigned a window. When a window isn't assigned, the default behavior is mirroring.
        guard session.role != .windowExternalDisplay else { return }
        /* the rest of your setup */
    }

When you don't assign a window, it seems that mirroring becomes the default option. Before that change, my external displays (screen mirroring) were given their own unique UIWindow instance.

I don't see this documented anywhere, and it is not intuitive. Because of this, I'm somewhat fearful that it will break in the future.

Hope it still helps.