在 上篇文章 中我们简单了解了 App、Scene,以及几个内置 Scene 的应用。在本文中,我们着重探讨在 SwiftUI 2.0 新的代码结构下如果更高效的组织 Data Flow。
新特性
@AppStorage
AppStorage 是苹果官方提供的用于操作 UserDefault 的属性包装器。这个功能在 Swift 提供了 propertyWrapper 特性后,已经有众多的开发者编写了类似的代码。功能上没有任何特别之处,不过名称对应了新的 App 协议,让人更容易了解其可适用的周期。
- 数据可持久化,app 退出后数据仍保留
- 仅包装了 UserDefault,数据可以 UserDefault 正常读取
- 可保存的数据类型同 UserDefault,不适合保存复杂类型数据
- 在 app 的任意 View 层级都可适用,不过在 app 层使用并不起作用(不报错)
@main
struct AppStorageTest: App {
//不报错,不过不起作用
//@AppStorage("count") var count = 0
var body: some Scene {
WindowGroup {
RootView()
CountView()
}
}
}
struct RootView: View {
@AppStorage("count") var count = 0
var body: some View {
List{
Button("+1"){
count += 1
}
}
}
}
struct CountView:View{
@AppStorage("count") var count = 0
var body: some View{
Text("Count:\(count)")
}
}
@SceneStorage
使用方法同@AppStorage 十分类似,不过其作用域仅限于当前 Scene。
- 数据作用域仅限于 Scene 中
- 生命周期同 Scene 一致,当前在 iPadOS 下,如果强制退出一个两分屏显示的 App, 系统在下次打开 App 时有时会保留上次的 Scene 信息。不过,如果如果单独退出一个 Scene,数据则失效
- 支持的类型基本等同于@AppStorage,适合保存轻量数据
- 比较适合保存基于 Scene 的特质信息,比如 TabView 的选择,独立布局等数据
@main
struct NewAllApp: App {
var body: some Scene {
WindowGroup{
ContentView1()
}
}
}
struct ContentView:View{
@SceneStorage("tabSeleted") var selection = 2
var body:some View{
TabView(selection:$selection){
Text("1").tabItem { Text("1") }.tag(1)
Text("2").tabItem { Text("2") }.tag(2)
Text("3").tabItem { Text("3") }.tag(3)
}
}
}
上述代码在 PadOS 下运行正常,不过在 macOS 下程序会报错。估计应该是 bug
Data Flow
手段
苹果在 SwiftUI 2.0 中添加了 @AppStorage @SceneStorage @StateObject 等新的属性包装器,我根据自己的理解对目前 SwiftUI 提供的部分属性包装器做了如下总结:
经过此次升级后,SwiftUI 已经大大的完善了各个层级数据的生命周期管理,对不同的类型、不同的场合、不同的用途都提供了解决方案,为编写符合 SwiftUI 的 Data Flow 提供了便利,我们可以根据自己的需要选择适合的 Source of truth 手段。
想了解其中的更多细节,可以参看我的其他文章:
ObservableObject 研究 —— 想说爱你不容易
变化
在 SwiftUI 1.0 中,我们通常会在 AppDelegate 中创建需要生命周期与 app 一致的数据(比如 CoreData 的 Container ),在 SceneDelegate 中创建 Store 之类的数据源,并通过。environmentObject 注入。不过随着 SwiftUI 2.0 在程序入口方面的变化,以及采取的全新 Delegate 响应方式,我们可以通过更简洁、清晰的代码完成上述工作。
@main
struct NewAllApp: App {
@StateObject var store = Store()
var body: some Scene {
WindowGroup{
ContentView()
.environmentObject(store)
}
}
}
class Store:ObservableObject{
@Published var count = 0
}
上述例子中,将
@StateObject var store = Store()
换成
let store = Store()
目前来说是一样的。
虽然目前 SceneBuilder、CommandBuilder 对 Dynamic update 和逻辑判断尚不支持,我相信应该在不久的将来,或许我们就可以使用类似下面的代码来完成很多有趣的工作了,当前代码无法执行
@main
struct NewAllApp: App {
@StateObject var store = Store()
@SceneBuilder var body: some Scene {
//@SceneBuilder 目前不支持判断,不过将来应该会加上
if store.scene == 0 {
WindowGroup{
ContentView1()
.environmentObject(store)
}
.onChange(of: store.number){ value in
print(value)
}
.commands{
CommandMenu("DynamicButton"){
//目前无法动态切换内容,怀疑是 bug,已反馈
switch store.number{
case 0:
Button("0"){}
case 1:
Button("1"){}
default:
Button("other"){}
}
}
}
else {
DocumentGroup(newDocment:TextFile()){ file in
TextEditorView(document:file.$document)
}
}
Settings{
VStack{
//可正常变换
Text("\(store.number)")
.padding(.all, 50)
}
}
}
}
struct ContentView1:View{
@EnvironmentObject var store:Store
var body:some View{
VStack{
Picker("select",selection:$store.number){
Text("0").tag(0)
Text("1").tag(1)
Text("2").tag(2)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
class Store:ObservableObject{
@Published var number = 0
@Published var scene = 0
}
跨平台代码
在 上篇文章 我们介绍了新的 @UIApplicationDelegateAdaptor 的使用方法,我们也可以直接创建一个支持 Delegate 的 store。
import SwiftUI
class Store:NSObject,ObservableObject{
@Published var count = 0
}
#if os(iOS)
extension Store:UIApplicationDelegate{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("launch")
return true
}
}
#endif
@main
struct AllInOneApp: App {
#if os(iOS)
@UIApplicationDelegateAdaptor(Store.self) var store
#else
@StateObject var store = Store()
#endif
@Environment(\.scenePhase) var phase
@SceneBuilder var body: some Scene {
WindowGroup {
RootView()
.environmentObject(store)
}
.onChange(of: phase){phase in
switch phase{
case .active:
print("active")
case .inactive:
print("inactive")
case .background:
print("background")
@unknown default:
print("for future")
}
}
#if os(macOS)
Settings{
Text("偏好设置").padding(.all, 50)
}
#endif
}
}
总结
在 ObservableObject 研究 —— 想说爱你不容易 中,我们探讨过 SwiftUI 更倾向于我们不要创建一个沉重的 Singel source of truth, 而是将每个功能模块作为独立的状态机(一起组合成一个大的状态 app),使用能够对生命周期和作用域更精确可控的手段创建区域性的 source of truth。
从 SwiftUI 第一个版本升级的内容来看,目前 SwiftUI 仍是这样的思路。