为什么这个方法调用必须用await标记,即使它不是异步方法?

Pet*_*ker 2 ios swift

如果我需要监听通知,我总是这样做:

NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
Run Code Online (Sandbox Code Playgroud)

但是,如果我在异步方法体内移动该行,编译器突然希望将该行标记为等待:

func setup() async {
    let importantStuff = await someTask()
    // store my important stuff, now setup is complete

    // This line won't compile without the await keyword.
    // But addObserver is not even an async method 
    await NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)

    // But this works fine
    listen()
}


func listen() {
    NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
Run Code Online (Sandbox Code Playgroud)

最小的例子

此代码无法编译(编译器想要添加awaitaddObserver 调用)。

class Test {
    
    func thisFailsToCompile() async {
        let stuff = try! await URLSession.shared.data(from: URL(string: "https://google.com")!)
        NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    
    @objc func foo() {
        
    }
}
Run Code Online (Sandbox Code Playgroud)

这里发生了什么?

Rob*_*ier 6

问题是 UIApplication 是@MainActor并且didEnterBackgroundNotification是它的一个属性,并且它没有被标记nonisolated,所以它只能在主要参与者上访问。这可能是 Apple 的疏忽,因为didEnterBackgroundNotification是一个常数,并且应该能够被标记nonisolated。但这也可能是 ObjC/Swift 桥接器的限制。

根据您希望它如何工作,有几种修复方法。

影响最小的更改(除了保留await)是同步缓存该值:

class Test {
    // Cache the value of didEnterBackgroundNotification so that async code doesn't have to access UIApplication
    private let didEnterBackgroundNotification = UIApplication.didEnterBackgroundNotification

    func thisFailsToCompile() async {
        NotificationCenter.default.addObserver(self, selector: #selector(foo), name: didEnterBackgroundNotification, object: nil)
    }

    @objc func foo() { }
}
Run Code Online (Sandbox Code Playgroud)

或者,您可以标记Test@MainActor. “UI”的东西几乎总是应该是@MainActor,所以这取决于它是什么类型的对象。

@MainActor 
class Test { ... }
Run Code Online (Sandbox Code Playgroud)

或者,您可以将相关方法标记为@MainActor(尽管这可能不是解决您问题的好方法):

    @MainActor func thisFailsToCompile() async {
        NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
Run Code Online (Sandbox Code Playgroud)

请注意,通知是在调用的队列上同步传递的postNotification。这意味着所有 UIKit(包括 UIApplication)通知都会到达主队列。事实上,大多数通知都来自主队列。这不是必需的或强制执行的(因此应用程序中的本地通知可能会出现在随机队列中),但这很常见。因此,如果您监听通知,那么创建该对象通常非常有用@MainActor

(但希望苹果也能解决这种关于各种常量被隔离的愚蠢问题。)