SwiftUI2 应用程序生命周期

长期以来,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,就可以了:

创建一个新的 SwiftUI 项目

让我们来看看一些常见的场景。

初始化资源,SDK 或框架

大多数应用程序在应用程序启动时需要执行几个步骤:获取一些配置值、连接到数据库或初始化框架或第三方SDK。

通常,您会在ApplicationDelegatesapplication(_: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中实现applicationDidBecomeActiveapplicationWillResignActiveapplicationDidEnterBackground

从 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 <your url>
demo.gif

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容

  • ![Flask](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW...
    极客学院Wiki阅读 7,229评论 0 3
  • 不知不觉易趣客已经在路上走了快一年了,感觉也该让更多朋友认识知道易趣客,所以就谢了这篇简介,已做创业记事。 易趣客...
    Physher阅读 3,407评论 1 2
  • 双胎妊娠有家族遗传倾向,随母系遗传。有研究表明,如果孕妇本人是双胎之一,她生双胎的机率为1/58;若孕妇的父亲或母...
    邺水芙蓉hibiscus阅读 3,694评论 0 2
  • 晴天,拥抱阳光,拥抱你。雨天,想念雨滴,想念你。 我可以喜欢你吗可以啊 我还可以喜欢你吗可以,可是你要知道我们不可...
    露薇霜凝阅读 1,200评论 1 2