Setter for 'screen' was deprecated in iOS 13.0

2.4k Views Asked by At

I was trying to follow this tutorial to create a multi screen application:

https://www.youtube.com/watch?v=UYviLiI2rlY&t=774s

Unfortunately at min 25:00 - 26:00 I receive an error and my external screen stay black:

[Assert] Error in UIKit client: -[UIWindow setScreen:] should not be called if the client adopts
UIScene lifecycle. Call -[UIWindow setWindowScene:] instead.

My code is:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var textView: UITextView!
    var additionalWindows = [UIWindow]()

    override func viewDidLoad() {
        super.viewDidLoad()

        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}
            let screenDimensions = newScreen.bounds

            let newWindow = UIWindow(frame: screenDimensions)
            newWindow.screen = newScreen

            guard let vc = self.storyboard?.instantiateViewController(withIdentifier: "PreviewViewController") as? PreviewViewController else {
                fatalError("Unable to find PreviewViewController")
            }

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


}

And I have a deprecation alert in newWindow.screen = newScreen : Setter for 'screen' was deprecated in iOS 13.0 but I can't find anything useful and not overcomplicated on how to solve this issue.

2

There are 2 best solutions below

0
On

Together with the answer provided here i was able to make it work in the simulator. It seems for iOS 13+ you have to find the scene in UIApplication.shared.connectedScenes.

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var textView: UITextView!
    var additionalWindows = [UIWindow]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        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() + 1) {
                // 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 {
                    fatalError("Connected scene was not found") // You might want to retry here after some time
                }
                let screenDimensions = newScreen.bounds
                
                let newWindow = UIWindow(frame: screenDimensions)
                newWindow.windowScene = connectedWindowScene
                
                guard let vc = self.storyboard?.instantiateViewController(withIdentifier: "PreviewViewController") as? PreviewViewController else {
                    fatalError("Unable to find PreviewViewController")
                }
                
                newWindow.rootViewController = vc
                newWindow.isHidden = false
                self.additionalWindows.append(newWindow)
            }
        }
    }
}

I'm not sure about the timing, .now() + 1 seems to work on simulator but I haven't tried on real hardware yet, so you might want to adjust this.

0
On

Note that you 'should' instantiate a VC as per the externalWindow.rootViewController

In my case, I used the external display for presenting a custom UIView() so I use an empty UIViewController() and then add my view to it.

private func setupExternalScreen(screen: UIScreen, retryUntilSet: Bool = true, retryTimesUntilDiscarded: Int = 0) {
    var matchingWindowScene: UIWindowScene? = nil
    let scenes = UIApplication.shared.connectedScenes
    for item in scenes {
        if let windowScene = item as? UIWindowScene {
            if (windowScene.screen == screen) {
                matchingWindowScene = windowScene
                break
            }
            }
    }
    if matchingWindowScene == nil {
        if true == retryUntilSet {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                if retryTimesUntilDiscarded < 5 {
                        self.setupExternalScreen(screen:screen, retryUntilSet: false, retryTimesUntilDiscarded += 1)
                } else {
                    let alert = UIAlertController(title: "Not connected", message: "Reconnect the display and try again", preferredStyle: .alert)
                        let ok = UIAlertAction(title: "Ok", style: .default, handler: nil)
                        alert.addAction(ok)
                        self.present(alert, animated: true, completion: nil)
                }
            }
        }
        return
    }
    externalWindow = UIWindow(frame: screen.bounds)
    externalWindow.rootViewController = UIViewController()
    airplayView.frame = externalWindow.frame
    if !externalWindow.subviews.contains(airplayView) {
        externalWindow.rootViewController?.view.addSubview(airplayView)
        if let _ = view.window {
            view.window?.makeKey()
        }
    } else {
        airplayView.updateValues()
    }

    externalWindow.windowScene = matchingWindowScene
    externalWindowWindow.isHidden = false
}

If your app requires iOS<13 you might need to use if #available(iOS 13.0, *) {} to decide how to setup your external screen.

Forgot to mention... externalWindow is declared inside the ViewController where I demand using the second screen

lazy var externalWindow = UIWindow()