UIApplication.shared.delegate在Xcode11中对应的SceneDelegate是什么?

100
我在SceneDelegate中定义了一个let属性。我希望一些ViewControllers能够在场景中访问它。 在UIKit中,我可以像这样访问App Delegate属性:
UIApplication.shared.delegate

那么,转换并指定属性名称...

从UIViewController的实例中获取对应视图控制器所在的SceneDelegate的引用,是否有相应的方法?

9个回答

81
自 iOS 13 起,UIApplication 有一个 connectedScenes 属性,它是 Set<UIScene>。每个场景都有一个 delegate,它是一个 UISceneDelegate。因此,您可以通过这种方式访问所有的代理。
一个场景可以管理一个或多个窗口 (UIWindow),您可以从其 windowScene 属性获取窗口的 UIScene
如果您想要特定视图控制器的场景代理,请注意以下内容。从 UIViewController 中,您可以从其视图中获取其窗口。从窗口中,您可以获取其场景,当然也可以从场景中获取其代理。
简而言之,从视图控制器中,您可以执行以下操作:
let mySceneDelegate = self.view.window.windowScene.delegate

然而,有很多时候视图控制器没有窗口。当视图控制器呈现另一个全屏视图控制器时,就会发生这种情况。当视图控制器在导航控制器中且视图控制器不是顶部可见视图控制器时,也可能会发生这种情况。
这需要一种不同的方法来查找视图控制器的场景。最终,您需要使用响应者链和视图控制器层次结构的组合,直到找到通向场景的路径。
以下扩展将(可能)从视图或视图控制器获取UIScene。一旦您拥有场景,就可以访问其委托。
请添加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
    }
}

这个方法可以从任何视图或视图控制器中调用,以获取其场景。但请注意,在viewDidAppear至少被调用一次之后,您只能从视图控制器中获取场景。如果您提前尝试,则视图控制器可能尚未成为视图控制器层次结构的一部分。

即使视图控制器的视图窗口为空,只要该视图控制器是视图控制器层次结构的一部分,并且在该层次结构的某个位置上,它附加到了一个窗口,这个方法也会起作用。


下面是UIResponder扩展的Objective-C实现:

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

1
我在self.view.window中有一个nil值...我该如何访问当前场景的当前窗口?就像“旧”的appDelegate.window一样。 - Fabiosoft
@Fabiosoft 我更新了这个答案,让你可以在更多情况下获取视图控制器的场景,即使窗口为空。 - rmaddy
这个在SwiftUI中的版本是什么? @rmaddy - Carla Camargo

67
如果您的应用程序只有一个场景,那么我可以使用以下方法使其正常工作:
let scene = UIApplication.shared.connectedScenes.first
if let sd : SceneDelegate = (scene?.delegate as? SceneDelegate) {
    sd.blah()
}

我在场景代理中处理OAuth回调的逻辑,并希望在回调发生时在ViewController中访问闭包。这段代码完美地允许我在ViewController中访问场景代理以访问该闭包。+1 - TheBrockEllis
5
只有在您的应用程序只有一个场景时,此内容才有效。支持场景的主要好处是您可以拥有多个场景。如果存在多个场景,像这样的代码将无法正常工作。 - rmaddy
1
这对于Azure的MSAL SDK集成非常有效。感谢您,为我们节省了一些编码时间,以便让MSAL身份验证模态可见。在UIApplication苹果文档中完全忽略了此属性。 - matthew
4
这是不正确的,你得到的是任何第一个连接的 UIScene,而不是包含 UIViewControllerUIScene 父级。 - Pedro Paulo Amorim
3
然而,这并不是正确的解决方法,因为你从全局控制器中获取了一个值,而不是从当前的UIScene中获取。 - Pedro Paulo Amorim
显示剩余2条评论

35

JoeGalind 谢谢你,我也是用类似的方法解决的。

// 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 */
}

使用强制解包是不正确的,这些属性被苹果标记为可选的是有原因的。过去,我曾经遇到过 UIApplication.shared.delegate as! AppDelegate 导致应用程序崩溃的情况。 - Egzon P.

11

如果您只是想查找窗口,则不需要引用 SceneDelegate

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

仅当您的应用程序只包含一个场景和一个窗口时,才应使用此方法。


10

iOS 13+、Swift

场景表示应用程序 UI 的实例,每个场景都维护着自己完全独立的状态。而应用程序本身只有单个实例,并保持对所有连接的场景的引用。并且场景是类,因此它们是引用类型。因此,在连接时仅需维护对场景本身的引用,然后在 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"
    }
}

1
这应该是被选中的答案!而且很有趣,因此它具备了这个优点。 - WikipediaBrown
当应用程序有多个活动场景时,这段代码无法正常工作。假设有场景A、B和C。假设场景A是最后一个连接的场景,因此currentScene被设置为场景A。现在假设场景C中的一个视图控制器想要获取其代理。这段代码将返回场景A的代理,而不是场景C的代理。 - HangarRash
@HangarRash,没错,但这个答案是针对单场景应用程序的 ;) - Serj Rubens
@SerjRubens 在哪里声称答案只适用于单场景应用程序?根据所写的内容,答案明显假设存在多个场景。这就是问题所在,因为它无法正确处理它们。顺便说一句 - 视图控制器可以轻松访问其场景。不需要任何这些代码。 - HangarRash
@SerjRubens 在哪里回答声称它只适用于单个场景应用程序?按照所写的,答案显然假设存在多个场景。这就是问题所在,因为它无法正确处理它们。顺便说一句 - 视图控制器可以轻松访问其场景。这些代码都是不需要的。 - undefined

9
我在一个静态弱引用的属性中存储了SceneDelegate的引用,并在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
    }
}

稍后,可以使用SceneDelegate.shared来访问场景。
更新:如评论中所提到的,这个解决方案只适用于处理单个场景的应用程序。在iPhone上运行的应用程序通常只有一个场景。iPad应用程序可以有多个场景。

1
我认为这应该是被接受的答案。干净、简单而安全。 - D. Kee
@D.Kee 但是这并没有考虑到一个应用程序可以有多个场景,每个场景都有自己的委托。 - HangarRash

2
我在我的MasterViewController中使用了这个:
- (void)viewDidLoad {
    [super viewDidLoad];

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

self.view.window在此时为nil,因此我不得不到达已经加载并添加到窗口的父级。

如果您正在使用分割视图控制器并且场景委托是分割委托,则可以避免使用窗口/场景,只需执行以下操作:

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

我在我的DetailViewController中使用了这个方法。


-1

Objective-C的版本:

    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);

-1

有时窗口会变成nil,您可以在属性列表(plist)下检查故事板名称,请参考图片 - enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接