UIApplication.shared.delegate equivalent for SceneDelegate xcode11?
Asked Answered
R

9

104

I have defined a let property within my SceneDelegate. I would like some of the ViewControllers to be able to access that within the scene.

In UIKit I could access App Delegate properties like this:

UIApplication.shared.delegate

then casting and specifying the property name...

Is there an equivalent to get the a reference to the SceneDelegate the view controller is in from the UIViewController's instance?

Ratcliff answered 13/6, 2019 at 21:33 Comment(0)
A
83

As of iOS 13, UIApplication has the connectedScenes property which is Set<UIScene>. Each of those scenes has a delegate which is a UISceneDelegate. So you could access all of the delegates that way.

A scene can manage one or more windows (UIWindow) and you can get a window's UIScene from its windowScene property.

If you want the scene delegate for a specific view controller then note the following. From a UIViewController you can get its window from its view. From the window you can get its scene and of course from the scene you can get its delegate.

In short, from a view controller, you can do:

let mySceneDelegate = self.view.window.windowScene.delegate

However, there are plenty of times where a view controller has no window. This happens when a view controller presents another full screen view controller. This can happened when the view controller is in a navigation controller and the view controller is not the top, visible view controller.

This requires a different approach to finding the view controller's scene. Ultimately you need to use a combination of walking the responder chain and the view controller hierarchy until you find a path that leads to the scene.

The following extension will (may) get you a UIScene from a view or view controller. Once you have the scene, you can access its delegate.

Add UIResponder+Scene.swift:

import UIKit

@available(iOS 13.0, *)
extension UIResponder {
    @objc var scene: UIScene? {
        return nil
    }
}

@available(iOS 13.0, *)
extension UIScene {
    @objc override var scene: UIScene? {
        return self
    }
}

@available(iOS 13.0, *)
extension UIView {
    @objc override var scene: UIScene? {
        if let window = self.window {
            return window.windowScene
        } else {
            return self.next?.scene
        }
    }
}

@available(iOS 13.0, *)
extension UIViewController {
    @objc override var scene: UIScene? {
        // Try walking the responder chain
        var res = self.next?.scene
        if (res == nil) {
            // That didn't work. Try asking my parent view controller
            res = self.parent?.scene
        }
        if (res == nil) {
            // That didn't work. Try asking my presenting view controller 
            res = self.presentingViewController?.scene
        }

        return res
    }
}

This can be called from any view or view controller to get its scene. But note that you can only get the scene from a view controller only after viewDidAppear has been called at least once. If you try any sooner then the view controller may not yet be part of the view controller hierarchy.

This will work even if the window of the view of the view controller is nil as long as the view controller is part of a view controller hierarchy and somewhere up that hierarchy, it is attached to a window.


Here is an Objective-C implementation of the UIResponder extension:

UIResponder+Scene.h:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIResponder (Scene)

@property (nonatomic, readonly, nullable) UIScene *scene API_AVAILABLE(ios(13.0));

@end

NS_ASSUME_NONNULL_END

UIResponder+Scene.m:

#import "ViewController+Scene.h"

@implementation UIResponder (Scene)

- (UIScene *)scene {
    return nil;
}

@end

@implementation UIScene (Scene)

- (UIScene *)scene {
    return self;
}

@end

@implementation UIView (Scene)

- (UIScene *)scene {
    if (self.window) {
        return self.window.windowScene;
    } else {
        return self.nextResponder.scene;
    }
}

@end

@implementation UIViewController (Scene)

- (UIScene *)scene {
    UIScene *res = self.nextResponder.scene;
    if (!res) {
        res = self.parentViewController.scene;
    }
    if (!res) {
        res = self.presentingViewController.scene;
    }

    return res;
}

@end
Ankledeep answered 13/6, 2019 at 22:10 Comment(3)
I have nil value for self.view.window... how can I access the current window of the current scene? Like the "old" appDelegate.windowAtelier
@Atelier I updated this answer that lets you get the view controller's scene under more conditions, even if the window is nil.Ankledeep
What is the version of this with SwiftUI? @AnkledeepPenholder
A
67

If your app will only ever have one scene then I was able to get it to work using this:

let scene = UIApplication.shared.connectedScenes.first
if let sd : SceneDelegate = (scene?.delegate as? SceneDelegate) {
    sd.blah()
}
Ambiguous answered 27/10, 2019 at 15:12 Comment(8)
I have the logic for handling OAuth callbacks in the Scene Delegate and a closure that I wanted to access in the ViewController when the callback occurs. This code perfectly allows me to access the Scene Delegate in the ViewController to access that closure. +1Principled
This is only valid if your app has one scene. The whole benefit of supporting scenes is that you can have more than one scene. Code like this won't work properly when there is more than one scene.Ankledeep
Yes. Of course. This is a "shortcut" for a one-scene project. You would have to go through the connectedScenes array if you wish to access more than one scene.Ambiguous
This worked really well for Azure's MSAL SDK integration. Thanks for this, saved us some time coding to get the MSAL authentication modal visible. Completely looked over this property in UIApplication apple documentation.Cidevant
This is incorrect, you are getting any first connected UIScene, not the UIScene parent of the UIViewController.Jodeejodhpur
@PedroPauloAmorim : this is a shortcut for 90% of projects that only have one UIScene. It's three lines of code to quickly achieve this.Ambiguous
Still, this is not the correct solution because you are getting a value from a global controller, not from your current UIScene.Jodeejodhpur
Its giving me AppSceneDelegate. But in my case, it is 'SceneDelegate'. So i m unable to cast AppSceneDelegate into SceneDelegateBettiebettina
S
37

JoeGalind Thank you I also solved in a similar way.

// iOS13 or later
if #available(iOS 13.0, *) {
    let sceneDelegate = UIApplication.shared.connectedScenes
        .first!.delegate as! SceneDelegate
    sceneDelegate.window!.rootViewController = /* ViewController Instance */

// iOS12 or earlier
} else {
    // UIApplication.shared.keyWindow?.rootViewController
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    appDelegate.window!.rootViewController = /* ViewController Instance */
}
Seamaid answered 7/3, 2020 at 11:46 Comment(2)
Using force unwrap is not right, there is a reason why these properties are marked as optional by Apple. In the past, I have faced that UIApplication.shared.delegate as! AppDelegate is crashing the app.Benedic
Its giving me AppSceneDelegate. But in my case, it is 'SceneDelegate'. So i m unable to cast AppSceneDelegate into SceneDelegateBettiebettina
F
12

You don't need to reference SceneDelegate if you are only trying to find the window:

(UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first

You should only use this approach if you app will only ever have one scene and one window.

Forethoughtful answered 11/1, 2021 at 23:17 Comment(0)
M
10

iOS 13+, Swift

Scenes represent instances of the app's UI and each scene maintains its own, fully-independent state. And the app itself, which is never more than a single instance, maintains references to all of these connected scenes. And scenes are classes so they are reference-types. Therefore, simply maintain a reference to the scene itself when it is connected and then find that scene in the set of connected scenes within UIApplication.shared.

// Create a globally-accessible variable of some kind (global
// variable, static property, property of a singleton, etc.) that
// references this current scene (itself).
var currentScene: UIScene?

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = scene as? UIWindowScene else {
            return
        }
        // Save the reference when the scene is born.
        currentScene = scene
    }
    
    func borat() {
        print("great success")
    }
}

// Here is a convenient view controller extension.
extension UIViewController {
    var sceneDelegate: SceneDelegate? {
        for scene in UIApplication.shared.connectedScenes {
            if scene == currentScene,
               let delegate = scene.delegate as? SceneDelegate {
                return delegate
            }
        }
        return nil
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        sceneDelegate?.borat() // "great success"
    }
}
Mariahmariam answered 30/10, 2021 at 17:49 Comment(4)
This should be the selected answer! Also, it's funny so it's got that going for it.Allhallowtide
This doesn't work when an app has multiple active scenes. Let's say there is scene A, B, and C. Let's say scene A is the last one that was connected so currentScene is set to scene A. Now let's say a view controller in scene C wants to get its delegate. This code will return the delegate for scene A, not scene C.Exemption
@HangarRash, that's right but this answer is made for single scene apps ;)Boak
@SerjRubens Where does the answer claim that it is only for single scene apps? As written, the answer clearly assumes multiple scenes. And that's the problem because it doesn't handle them correctly. BTW - a view controller has easy access to its scene. None of this code is needed.Exemption
F
9

I store a reference of the SceneDelegate in a static weak property and initialise it in scene(:willConnectTo:options)

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    static weak var shared: SceneDelegate?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        Self.shared = self
    }
}

Later, scene can be accessed using SceneDelegate.shared


UPDATE: As mentioned in the comments, this solution will only work for apps that deal with only one scene. Apps that run on iPhone typically have only one scene. iPad apps can have multiple scenes.

Frisch answered 12/4, 2021 at 10:51 Comment(2)
This should be the accepted answer imo. Clean, simple and safeAirlee
@D.Kee But this doesn't account for the fact that an app can have multiple scenes each with their own delegate.Exemption
B
2

I'm using this in my MasterViewController:

- (void)viewDidLoad {
    [super viewDidLoad];

    SceneDelegate *sceneDelegate = (SceneDelegate *)self.parentViewController.view.window.windowScene.delegate;
}

self.view.window is nil at this point so I had to reach to the parent whose view has already been loaded and added to the window.

If you are using split view controller and the scene delegate is the split delegate then you could avoid the window/scene altogether and just do:

SceneDelegate *sceneDelegate = (SceneDelegate *)self.splitViewController.delegate;

I'm using this in my DetailViewController.

Blat answered 8/4, 2020 at 21:50 Comment(0)
N
-1

sometime window is getting nil you can check storyboard name under plist, please refer the image - enter image description here

Nippur answered 8/10, 2021 at 7:32 Comment(0)
S
-1

the Objective-C version:

    NSSet<UIScene *> * sceneArr = [[UIApplication sharedApplication] connectedScenes];
    UIScene * scene = [[sceneArr allObjects] firstObject];
    NSObject * sceneDelegate = (NSObject *)scene.delegate;

    // for example, try to get variable "window"
    UIWindow *currentKeyWindow = [sceneDelegate valueForKey: @"window"];
    NSLog(@"%@", currentKeyWindow);
Sideways answered 27/4, 2022 at 7:2 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.