長期以來,iOS 開發(fā)者一直使用AppDelegates
作為他們應(yīng)用程序的主要入口點(diǎn)。隨著 SwiftUI2 在 WWDC 2020 上的推出,Apple 引入了一個(gè)新的應(yīng)用程序生命周期,它(幾乎)完全取消了AppDelegate
,為類似 DSL 的方法讓路。
在本文中,我將討論為何引入此更改,以及如何在新應(yīng)用或現(xiàn)有應(yīng)用中利用新生命周期。
指定應(yīng)用程序入口點(diǎn)
我們需要回答的第一個(gè)問題是,我們?nèi)绾胃嬖V編譯器我們應(yīng)用程序的入口點(diǎn)?SE-0281指定了基于類型的程序入口點(diǎn)的工作方式:
Swift 編譯器將使用
@main
屬性注釋的類型標(biāo)注為程序的入口點(diǎn)。用@main
標(biāo)記的類型有一個(gè)隱式要求:聲明一個(gè)靜態(tài)main()
方法。
創(chuàng)建新的 SwiftUI 應(yīng)用程序時(shí),應(yīng)用程序的main class 如下所示:
import SwiftUI
@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
那么SE-0281中提到的靜態(tài)函數(shù)main()
在哪里呢?
好吧,事實(shí)證明,框架提供者可以(并且應(yīng)該)為用戶的方便提供默認(rèn)實(shí)現(xiàn)。查看上面的代碼片段,您會注意到它SwiftUIAppLifeCycleApp
遵循App
協(xié)議。Apple 提供了如下所示的協(xié)議擴(kuò)展:
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension App {
/// Initializes and runs the app.
///
/// If you precede your ``SwiftUI/App`` conformer's declaration with the
/// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626)
/// attribute, the system calls the conformer's `main()` method to launch
/// the app. SwiftUI provides a
/// default implementation of the method that manages the launch process in
/// a platform-appropriate way.
public static func main()
}
我們有了它——這個(gè)協(xié)議擴(kuò)展提供了一個(gè)默認(rèn)的實(shí)現(xiàn)來處理應(yīng)用程序的啟動。
由于 SwiftUI 框架不是開源的,我們無法看到 Apple 如何實(shí)現(xiàn)這一點(diǎn),但Swift Argument Parser是開源的,并且也使用這種方法。查看源代碼ParsableCommand
以了解他們?nèi)绾问褂脜f(xié)議擴(kuò)展來提供main
作為程序入口點(diǎn)的靜態(tài)函數(shù)的默認(rèn)實(shí)現(xiàn):
extension ParsableCommand {
...
public static func main(_ arguments: [String]?) {
do {
var command = try parseAsRoot(arguments)
try command.run()
} catch {
exit(withError: error)
}
}
public static func main() {
self.main(nil)
}
}
如果所有這些聽起來有點(diǎn)復(fù)雜,那么好消息是您在創(chuàng)建新的 SwiftUI 應(yīng)用程序時(shí)實(shí)際上不必?fù)?dān)心它:只需確保在創(chuàng)建應(yīng)用程序時(shí)在Life Cycle下拉列表中選擇SwiftUI App,就可以了:
讓我們來看看一些常見的場景。
初始化資源,SDK 或框架
大多數(shù)應(yīng)用程序在應(yīng)用程序啟動時(shí)需要執(zhí)行幾個(gè)步驟:獲取一些配置值、連接到數(shù)據(jù)庫或初始化框架或第三方SDK。
通常,您會在ApplicationDelegates
的 application(_:didFinishLaunchingWithOptions:)
方法中執(zhí)行此操作。由于我們不再有application delegate,我們需要找到其他方法來初始化我們的應(yīng)用程序。根據(jù)您的具體要求,這里有一些策略:
@main
struct ColorsApp: App {
init() {
print("Colors application is starting up. App initialiser.")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
如果這些都不能滿足您的需求,可能你真的需要AppDelegate
。最后會介紹如何引入AppDelegate
的相關(guān)方法。
應(yīng)用程序的生命周期
有時(shí)能夠知道您的應(yīng)用程序處于哪種狀態(tài)很有用。例如,您可能希望在應(yīng)用程序變?yōu)榛顒訝顟B(tài)active
時(shí)立即獲取新數(shù)據(jù),或者在應(yīng)用程序變?yōu)榉腔顒訝顟B(tài)inactive
并轉(zhuǎn)換到后臺background
時(shí)刷新所有緩存。
通常情況下,你會在你的ApplicationDelegate
中實(shí)現(xiàn)applicationDidBecomeActive
,applicationWillResignActive
或applicationDidEnterBackground
。
從 iOS 14.0 開始,Apple 提供了一個(gè)新的 API,允許以更優(yōu)雅和可維護(hù)的方式跟蹤應(yīng)用程序的狀態(tài):ScenePhase
. 您的項(xiàng)目可以有多個(gè)場景,但您可能只有一個(gè)場景,用WindowGroup
.
SwiftUI 跟蹤環(huán)境中場景的狀態(tài),您可以通過使用@Environment
屬性包裝器獲取當(dāng)前值,然后使用onChange(of:)
修飾符來偵聽任何更改,從而使代碼可以訪問當(dāng)前值:
@main
struct SwiftUIAppLifeCycleApp: App {
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
print("App is active")
case .inactive:
print("App is inactive")
case .background:
print("App is in background")
@unknown default:
print("Oh - interesting: I received an unexpected new value.")
}
}
}
}
值得注意的是,您也可以從應(yīng)用程序的其他位置讀取phase
。在應(yīng)用程序的頂層讀取phase
時(shí)(如代碼片段中所示),您將獲得應(yīng)用程序中所有phase
的匯總。.inactive
的值表示您的應(yīng)用程序中沒有任何場景處于活動狀態(tài)。讀取視圖上的phase
時(shí),您將收到包含該視圖的phase
的值。請記住,此時(shí)您的應(yīng)用程序可能包含具有其他phase
值的其他場景。有關(guān)scenephase
的更多詳細(xì)信息,請閱讀 Apple 的文檔。
deep links
以前,在處理deep links時(shí),您必須實(shí)現(xiàn)application(_:open:options:)
,并將傳入的URL路由到最合適的處理程序。
使用新的應(yīng)用程序生命周期模型,這變得容易多了。您可以通過將onOpenURL
修飾符附加到應(yīng)用程序的最頂層場景來處理傳入的 URL :
@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
print("Received URL: \(url)")
}
}
}
}
真正酷的是:您可以在整個(gè)應(yīng)用程序中安裝多個(gè)URL處理程序 - 使deep links更容易,因?yàn)槟梢栽谧詈线m的地方處理傳入鏈接。
如果可能,您應(yīng)該使用universal links (或Firebase 動態(tài)鏈接,它使用iOS 應(yīng)用程序的通用鏈接),因?yàn)樗鼈兪褂藐P(guān)聯(lián)的域名在您擁有的網(wǎng)站和您的應(yīng)用程序之間創(chuàng)建連接 - 這將允許您安全地共享數(shù)據(jù)。
但是,您仍然可以使用自定義URL schemes來鏈接到您的應(yīng)用程序中的內(nèi)容。
無論哪種方式,在您的應(yīng)用程序中觸發(fā)深層鏈接的一種簡單方法是在您的開發(fā)機(jī)器上使用以下命令:
xcrun simctl openurl booted <your url>
Continuing user activities
如果應(yīng)用程序使用NSUserActivity
,以與Siri, Handoff, or Spotlight整合,你需要處理用戶活動的延續(xù)。
同樣,新的應(yīng)用程序生命周期模型通過提供兩個(gè)修飾符使您可以更輕松地進(jìn)行此操作,這些修飾符允許您advertise活動并在以后繼續(xù)該活動。
下面是一個(gè)片段,展示了如何宣傳活動,例如,在詳細(xì)信息視圖中:
struct ColorDetailsView: View {
var color: String
var body: some View {
Image(color)
// ...
.userActivity("showColor" ) { activity in
activity.title = color
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// ...
}
}
}
為了繼續(xù)這個(gè)活動,你可以在你的頂級導(dǎo)航視圖中注冊一個(gè)onContinueUserActivity
閉包,像這樣:
import SwiftUI
struct ContentView: View {
var colors = ["Red", "Green", "Yellow", "Blue", "Pink", "Purple"]
@State var selectedColor: String? = nil
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(colors, id: \.self) { color in
NavigationLink(destination: ColorDetailsView(color: color),
tag: color,
selection: $selectedColor) {
Image(color)
}
}
}
.onContinueUserActivity("showColor") { userActivity in
if let color = userActivity.userInfo?["colorName"] as? String {
selectedColor = color
}
}
}
}
}
}
以上都不適合我怎么辦!
并非所有AppDelegate
的回調(diào)都受新應(yīng)用程序生命周期的支持(目前)。如果以上都不能滿足您的需求,那么您可能真的需要一個(gè)AppDelegate
。
您可能需要AppDelegate
的另一個(gè)原因是,您是否使用任何第三方 SDK,這些SDK利用method swizzling將自身注入應(yīng)用程序生命周期。Firebase是一個(gè)眾所周知的案例。
為了幫助您,Swift 提供了一種將遵循AppDelegate
協(xié)議的代理與您的App
連接的方法:@UIApplicationDelegateAdaptor
. 以下是如何使用它:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Colors application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.")
return true
}
}
@main
struct ColorsApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
如果您復(fù)制現(xiàn)有AppDelegate
實(shí)現(xiàn),請不要?jiǎng)h除@main
- 否則,編譯器會抱怨多個(gè)應(yīng)用程序入口點(diǎn)。
結(jié)論
有了這一切,讓我們來討論一下 Apple 做出這一改變的原因。我認(rèn)為有以下幾個(gè)原因:
SE-0281明確指出,設(shè)計(jì)目標(biāo)之一是提供一種更通用和輕量級的機(jī)制,用于將程序的入口點(diǎn)委托給指定類型。
Apple 為處理應(yīng)用程序生命周期而選擇的基于DSL的方法與在 SwiftUI 中構(gòu)建 UI 的聲明式方法非常吻合。使用相同的概念使事情更容易理解,并有助于新開發(fā)人員的上手。
任何聲明式方法的主要好處是:框架/平臺提供者不會將實(shí)現(xiàn)特定功能的負(fù)擔(dān)推給開發(fā)人員,而是負(fù)責(zé)解決這個(gè)問題。如果需要進(jìn)行任何更改,在不破壞許多開發(fā)人員的應(yīng)用程序的情況下發(fā)布這些更改會容易得多 - 理想情況下,開發(fā)人員不必更改他們的實(shí)現(xiàn),因?yàn)榭蚣軙槟幚硪磺小?/p>
總體而言,新的應(yīng)用程序生命周期模型使您的應(yīng)用程序啟動實(shí)施變得更容易、更簡單。你的代碼會更干凈、更容易維護(hù)——如果你問我,我認(rèn)為這是一件好事。
翻譯自The Ultimate Guide to the SwiftUI 2 Application Life Cycle