0

I am able to detect when a screen is detected, associate it with an appropriate windowScene and add a view to it. Slightly hacky but approximately working (code for disconnection not included here), thanks to this SO question:

class ExternalViewController: UIViewController {
  override func viewDidLoad() {
    view.backgroundColor = .cyan
    print("external frame \(view.frame.width)x\(view.frame.height)")
  }
}
class ViewController: UIViewController {
  var additionalWindows: [UIWindow] = []
  override func viewDidLoad() {
    //nb, Apple documentation seems out of date.
    //https://stackoverflow.com/questions/61191134/setter-for-screen-was-deprecated-in-ios-13-0
    NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, object: nil, queue: nil) { [weak self] notification in
      guard let self = self else {return}

      guard let newScreen = notification.object as? UIScreen else {return}
      // Give the system time to update the connected scenes
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
        // Find matching UIWindowScene
        let matchingWindowScene = UIApplication.shared.connectedScenes.first {
        guard let windowScene = $0 as? UIWindowScene else { return false }
        return windowScene.screen == newScreen
      } as? UIWindowScene

        guard let connectedWindowScene = matchingWindowScene else {
          NSLog("--- Connected scene was not found ---")
          return
          //fatalError("Connected scene was not found") // You might want to retry here after some time
        }
        let screenDimensions = newScreen.bounds

        let newWindow = UIWindow(frame: screenDimensions)
        NSLog("newWindow \(screenDimensions.width)x\(screenDimensions.height)")
        newWindow.windowScene = connectedWindowScene

        let vc = ExternalViewController()
        vc.mainVC = self

        newWindow.rootViewController = vc
        newWindow.isHidden = false
        self.additionalWindows.append(newWindow)
      }
    }
  }
}

When I do this in the iOS simulator, I see my graphics fill the screen as intended, but when running on my actual device, it appears with a substantial black border around all sides.

Note that this is not the usual border seen with the default display mirroring behaviour - the 16:9 aspect ratio is preserved, and I do see different graphics as expected, (flat cyan color in my example code, normally I'm doing some Metal rendering that has some slight anomalies that are out of scope here, although perhaps might lead to some different clues on this if I dig into it deeper).

The print messages report the expected 1920x1080 dimensions. I don't know UIKit very well, and haven't been doing much active Apple development (I'm dusting off a couple of old side projects here in the hopes of being able to use them to project visuals at a gig in the near future), so I don't know if there's something else to do with sizing constraints etc that I might be missing, but even so it's hard to see why it would behave differently in the simulator.

Other apps I have installed from the app store do indeed show fullscreen graphics on the external display - Netflix shows fullscreen video as you would expect, Concepts shows a different representation of the document than the one you see on the device.

PeterT
  • 1,454
  • 1
  • 12
  • 22

1 Answers1

0

So, in this instance the issue is to do with Overscan Compensation. Thanks to Jerrot on Discord for pointing me in the right direction.

In the context of my app, it is sufficient to add newScreen.overscanCompensation = .none in the connection notification delegate (actually, in the part that is delayed a few ms after that - it doesn't work if applied directly in the connection notification). In the question linked above, there is further discussion of other aspects that may be important in a different context.

This is my ViewController modified to achieve the desired result:

class ViewController: UIViewController {
  var additionalWindows: [UIWindow] = []
  override func viewDidLoad() {
    //nb, Apple documentation seems out of date.
    //https://stackoverflow.com/questions/61191134/setter-for-screen-was-deprecated-in-ios-13-0
    NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, object: nil, queue: nil) { [weak self] notification in
      guard let self = self else {return}

      guard let newScreen = notification.object as? UIScreen else {return}
      // Give the system time to update the connected scenes
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
        // Find matching UIWindowScene
        let matchingWindowScene = UIApplication.shared.connectedScenes.first {
        guard let windowScene = $0 as? UIWindowScene else { return false }
        return windowScene.screen == newScreen
      } as? UIWindowScene

        guard let connectedWindowScene = matchingWindowScene else {
          NSLog("--- Connected scene was not found ---")
          return
          //fatalError("Connected scene was not found") // You might want to retry here after some time
        }
        let screenDimensions = newScreen.bounds
      ////// new code here --->  
        newScreen.overscanCompensation = .none
      //////
        let newWindow = UIWindow(frame: screenDimensions)
        NSLog("newWindow \(screenDimensions.width)x\(screenDimensions.height)")
        newWindow.windowScene = connectedWindowScene

        let vc = ExternalViewController()
        vc.mainVC = self

        newWindow.rootViewController = vc
        newWindow.isHidden = false
        self.additionalWindows.append(newWindow)
      }
    }
  }
}

In this day and age, I find it pretty peculiar that overscan compensation is enabled by default.

PeterT
  • 1,454
  • 1
  • 12
  • 22