长期以来,iOS 开发者一直使用AppDelegates
作为他们应用程序的主要入口点。随着 SwiftUI2 在 WWDC 2020 上的推出,Apple 引入了一个新的应用程序生命周期,它(几乎)完全取消了AppDelegate
,为类似 DSL 的方法让路。
在本文中,我将讨论为何引入此更改,以及如何在新应用或现有应用中利用新生命周期。
指定应用程序入口点
我们需要回答的第一个问题是,我们如何告诉编译器我们应用程序的入口点?SE-0281指定了基于类型的程序入口点的工作方式:
Swift 编译器将使用
@main
属性注释的类型标注为程序的入口点。用@main
标记的类型有一个隐式要求:声明一个静态main()
方法。
创建新的 SwiftUI 应用程序时,应用程序的main class 如下所示:
import SwiftUI
@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
那么SE-0281中提到的静态函数main()
在哪里呢?
好吧,事实证明,框架提供者可以(并且应该)为用户的方便提供默认实现。查看上面的代码片段,您会注意到它SwiftUIAppLifeCycleApp
遵循App
协议。Apple 提供了如下所示的协议扩展:
@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()
}
我们有了它——这个协议扩展提供了一个默认的实现来处理应用程序的启动。
由于 SwiftUI 框架不是开源的,我们无法看到 Apple 如何实现这一点,但Swift Argument Parser是开源的,并且也使用这种方法。查看源代码ParsableCommand
以了解他们如何使用协议扩展来提供main
作为程序入口点的静态函数的默认实现:
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)
}
}
如果所有这些听起来有点复杂,那么好消息是您在创建新的 SwiftUI 应用程序时实际上不必担心它:只需确保在创建应用程序时在Life Cycle下拉列表中选择SwiftUI App,就可以了:
让我们来看看一些常见的场景。
初始化资源,SDK 或框架
大多数应用程序在应用程序启动时需要执行几个步骤:获取一些配置值、连接到数据库或初始化框架或第三方SDK。
通常,您会在ApplicationDelegates
的 application(_:didFinishLaunchingWithOptions:)
方法中执行此操作。由于我们不再有application delegate,我们需要找到其他方法来初始化我们的应用程序。根据您的具体要求,这里有一些策略:
- 在您的main class上实现初始化程序(请参阅文档)
- 设置存储属性的初始值(参见文档)
- 使用闭包设置默认属性值(请参阅文档)
@main
struct ColorsApp: App {
init() {
print("Colors application is starting up. App initialiser.")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
如果这些都不能满足您的需求,可能你真的需要AppDelegate
。最后会介绍如何引入AppDelegate
的相关方法。
应用程序的生命周期
有时能够知道您的应用程序处于哪种状态很有用。例如,您可能希望在应用程序变为活动状态active
时立即获取新数据,或者在应用程序变为非活动状态inactive
并转换到后台background
时刷新所有缓存。
通常情况下,你会在你的ApplicationDelegate
中实现applicationDidBecomeActive
,applicationWillResignActive
或applicationDidEnterBackground
。
从 iOS 14.0 开始,Apple 提供了一个新的 API,允许以更优雅和可维护的方式跟踪应用程序的状态:ScenePhase
. 您的项目可以有多个场景,但您可能只有一个场景,用WindowGroup
.
SwiftUI 跟踪环境中场景的状态,您可以通过使用@Environment
属性包装器获取当前值,然后使用onChange(of:)
修饰符来侦听任何更改,从而使代码可以访问当前值:
@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.")
}
}
}
}
值得注意的是,您也可以从应用程序的其他位置读取phase
。在应用程序的顶层读取phase
时(如代码片段中所示),您将获得应用程序中所有phase
的汇总。.inactive
的值表示您的应用程序中没有任何场景处于活动状态。读取视图上的phase
时,您将收到包含该视图的phase
的值。请记住,此时您的应用程序可能包含具有其他phase
值的其他场景。有关scenephase
的更多详细信息,请阅读 Apple 的文档。
deep links
以前,在处理deep links时,您必须实现application(_:open:options:)
,并将传入的URL路由到最合适的处理程序。
使用新的应用程序生命周期模型,这变得容易多了。您可以通过将onOpenURL
修饰符附加到应用程序的最顶层场景来处理传入的 URL :
@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
print("Received URL: \(url)")
}
}
}
}
真正酷的是:您可以在整个应用程序中安装多个URL处理程序 - 使deep links更容易,因为您可以在最合适的地方处理传入链接。
如果可能,您应该使用universal links (或Firebase 动态链接,它使用iOS 应用程序的通用链接),因为它们使用关联的域名在您拥有的网站和您的应用程序之间创建连接 - 这将允许您安全地共享数据。
但是,您仍然可以使用自定义URL schemes来链接到您的应用程序中的内容。
无论哪种方式,在您的应用程序中触发深层链接的一种简单方法是在您的开发机器上使用以下命令:
xcrun simctl openurl booted
Continuing user activities
如果应用程序使用NSUserActivity
,以与Siri, Handoff, or Spotlight整合,你需要处理用户活动的延续。
同样,新的应用程序生命周期模型通过提供两个修饰符使您可以更轻松地进行此操作,这些修饰符允许您advertise活动并在以后继续该活动。
下面是一个片段,展示了如何宣传活动,例如,在详细信息视图中:
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
// ...
}
}
}
为了继续这个活动,你可以在你的顶级导航视图中注册一个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
的回调都受新应用程序生命周期的支持(目前)。如果以上都不能满足您的需求,那么您可能真的需要一个AppDelegate
。
您可能需要AppDelegate
的另一个原因是,您是否使用任何第三方 SDK,这些SDK利用method swizzling将自身注入应用程序生命周期。Firebase是一个众所周知的案例。
为了帮助您,Swift 提供了一种将遵循AppDelegate
协议的代理与您的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()
}
}
}
如果您复制现有AppDelegate
实现,请不要删除@main
- 否则,编译器会抱怨多个应用程序入口点。
结论
有了这一切,让我们来讨论一下 Apple 做出这一改变的原因。我认为有以下几个原因:
SE-0281明确指出,设计目标之一是提供一种更通用和轻量级的机制,用于将程序的入口点委托给指定类型。
Apple 为处理应用程序生命周期而选择的基于DSL的方法与在 SwiftUI 中构建 UI 的声明式方法非常吻合。使用相同的概念使事情更容易理解,并有助于新开发人员的上手。
任何声明式方法的主要好处是:框架/平台提供者不会将实现特定功能的负担推给开发人员,而是负责解决这个问题。如果需要进行任何更改,在不破坏许多开发人员的应用程序的情况下发布这些更改会容易得多 - 理想情况下,开发人员不必更改他们的实现,因为框架会为您处理一切。
总体而言,新的应用程序生命周期模型使您的应用程序启动实施变得更容易、更简单。你的代码会更干净、更容易维护——如果你问我,我认为这是一件好事。
Github示例代码。
翻译自The Ultimate Guide to the SwiftUI 2 Application Life Cycle