iOS开发SwiftUI

SwiftUI2.0 —— App、Scene、新的代码结构(二)

转载:https://www.fatbobman.com/posts/swiftui2-new-feature-2/

在 上篇文章 中我们简单了解了 App、Scene,以及几个内置 Scene 的应用。在本文中,我们着重探讨在 SwiftUI2.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 一致,当前在 PadOS 下,如果强制退出一个两分屏显示的 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

  • 手段

苹果在 SwiftUI2.0 中添加了@AppStorage @SceneStorage @StateObject 等新的属性包装器,我根据自己的理解对目前 SwiftUI 提供的部分属性包装器做了如下总结:

经过此次升级后,SwiftUI 已经大大的完善了各个层级数据的生命周期管理,对不同的类型、不同的场合、不同的用途都提供了解决方案,为编写符合 SwiftUI 的 Data Flow 提供了便利,我们可以根据自己的需要选择适合的 Source of truth 手段。

  • 变化

在 SwiftUI1.0 中,我们通常会在 AppDelegate 中创建需要生命周期与 app 一致的数据(比如 CoreData 的 Container),在 SceneDelegate 中创建 Store 之类的数据源,并通过。environmentObject 注入。不过随着 SwiftUI2.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 仍是这样的思路。