下载链接:
https://download.csdn.net/download/gwh111/88670868
版本 1.0 (2023 年 4月)
更新说明:
4.7 修改问题和缺陷
4.13 新增缺陷讨论
4.14 新增引言和部分小结
4.17 高亮关键性代码,新增SwiftUI状态官方定义
目录
引言
声明式UI简介
@是什么
页面级变量的状态管理
应用级变量的状态管理
抽象
组件&合成
生命周期
扩展
开发模式
总结
参考资料
引言
本文通过ArkUI和SwiftUI对比的方式介绍两者的异同点,同时也可以作为给iOS SwiftUI开发者快速过度到ArkUI开发提供一定的参考。
想要使用ArkUI编写移动应用程序的SwiftUI开发人员可以查看本指南。它解释了如何将现有的SwiftUI知识应用于ArkUI。使用ArkUI进行构建时,您的SwiftUI知识和经验非常宝贵。
本文更多的是客观的特性比较,不同特性有不同特点,在不同场景下优缺点不同,很难讲ArkUI的特性好而SwiftUI某个特性不好,我们把同类的特性放在一起对比更能体现它们的异同。一门语言的理论设计和实际开发使用也会有很大差异,作者也会通过个人开发经验发表一些观点。
本文主要讨论了两者开发语言的特性以及状态管理。以一些例子论证描述的属性和特征。有些示例代码比较长,第一遍阅读时不需要过多关注所有代码,只需关注标黄的关键字,可以帮助你快速理解这个特性。而不截取部分代码比较是为了深入理解特性时缺少上下文完整性,添加完整代码的目的是提供一个完整的上下文,对于整体理解更有帮助。
声明式UI简介
ArkUI和SwiftUI代码描述了UI的外观和工作方式。开发人员将这种类型的代码称为声明性框架。
声明式UI:
如果说指令式是教会计算机 “怎么做”,那么声明式就是告诉计算机要 “做什么”。指令式编程是描述过程,期望程序执行以得到我们想要的结果;而声明式编程则是描述结果,让计算机为我们考虑和组织出具体过程,最后得到被描述的结果。
SwiftUI:
-1 SwiftUI 的 view 是值,而非对象:它们是不可变的,用来暂时描述屏幕上应该显示什么。-2 我们在一个 view 上几乎所有的方法调用 (像是 frame 或 background) 都会将 view 包装在一个修饰器中。因此,这些调用的顺序很重要,这一点和 UIView 里的属性不同。
-3 布局是自上而下的:父 view 向子 view 提供它们的可用空间,子 view 基于这个空间来
决定自己的尺寸。
-4 我们不能直接更新屏幕上的内容。相反,我们必须修改状态属性 (比如 @State 或
@ObservedObject),然后让 SwiftUI 去找出 view 树的变化方式。
借助Copy-on-Write特性优化内存管理
-1 Copy-on-Write 是一种用来优化占用内存大的值类型的拷贝操作的机制。
-2 对于Int,Double,String 等基本类型的值类型,它们在赋值的时候就会发生拷贝。(内存增加)
-3 对于 Array、Dictionary、Set 类型,当它们赋值的时候不会发生拷贝,只有在修改的之后才会发生拷贝。(内存按需延时增加)
-4 对于自定义的数据类型不会自动实现COW,可按需实现。
ArkUI:
-1 代表 UI 层的 View,一般来说并不是真实负责渲染的传统意义的视图层级,而
是一个 “虚拟的” 对 View 组织关系的描述 (声明)。
-2 决定 UI 的用户状态 State 被存储在某个或某几个对象中。
-3 用一个函数描述 View,这个函数的输入参数是 State,即 View = f(State)。
-4 框架在 State 改变时,调用上述函数获取对应新的 State 的 View,并与当前
的 View 进行差分计算,并重新渲染更改的部分。
@是什么
SwiftUI ArkUI
@Property Wrappers = struct @生成器函数
wrappedValue 只读的单向传递
projectedValue 可变更的双向传递
objectWillChange.send()
Get/set 方法
SwiftUI
Property Wrappers结构
A property wrapper is actually a struct .(属性包装器)
这个特殊的 struct 封装了⼀些模板⾏为应⽤到它们wrap的vars上:
1.@Published var emojiArt:EmojiArt = EmojiArt()
2.//… is really just this struct …
3.struct Published {
4. var wrappedValue: EmojiArt
5. var projectedValue: Publisherprojected value
的类型取决于wrapper⾃⼰,⽐如本例就是⼀个Publisher
6.}
7.//… and Swift (approximately) makes these vars available to you …
8.
9.var _emojiArt:Published = Published(wrappedValue:EmojiArt())
10.var emojiArt: EmojiArt {
11. get { _emojiArt.wrappedValue }
12. set {_emojiArt.wrappedValue = newValue }
13.}
把get,set直接通过 $emojiArt (即projectedValue)来使⽤。
当⼀个 Published 值发⽣变化:
1.It publishes the change through its projectedValue ( $emojiArt ) which is a Publisher .
2.It also invokes objectWillChange.send() in its enclosing ObservableObject
ArkUI
结构
页面级变量的状态管理
SwiftUI
@State、@StateObject 、@ObservedObject、@Binding、@Publisher
ArkUI
@State、@Prop、@Link、@Provide、@Consume、@ObjectLink、@Observed和@Watch用于管理页面级变量的状态。
SwiftUI ArkUI
@State @State+@Prop
SwiftUI
@State官方定义:
A property wrapper type that can read and write a value managed by SwiftUI.
在堆创建,指向堆
This is actually going to make some space in the heap for this.
@state 本身还是不可变 指针不变指向同一块内存 这块内存可变 不会经常使用 用于临时状态(触摸中 拖动保持状态)
1.@State private var somethingTemporary: SomeType // this can be of any type
1.private 表示别⼈访问不到
2.@State的的变化会在必要时引起重绘 (相当于⼀个 @ObservedObject )
3.view会不断销毁和重建 -> 指针会永远指向新的内存地址
4.⽽state是在堆上分配的空间
5.所以销毁和重建view并不会丢失state
和一般的存储属性不同,@State 修饰的值,在 SwiftUI 内部会被自动转换为一对
setter 和 getter,对这个属性进行赋值的操作将会触发 View 的刷新,它的 body 会
被再次调用,底层渲染引擎会找出界面上被改变的部分,根据新的属性值计算出新的
View,并进行刷新。
ArkUI
@State
@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。
1.@State title: Model = { value: ‘Hello World’ }
1.支持多种类型数据:支持class、number、boolean、string强类型数据的值类型和引用类型,以及这些强类型构成的数组,即Array、Array、Array、Array。不支持object和any。
2.支持多实例:组件不同实例的内部状态数据独立。
3.内部私有:标记为@State的属性是私有变量,只能在组件内访问。
4.需要本地初始化:必须为所有@State变量分配初始值,变量未初始化可能导致未定义的框架异常行为。
5.创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@State状态变量的初始值。
@Prop
1.@Prop count: number
单向数据绑定。
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
SwiftUI ArkUI
@State+@Binding @State+@Prop+@Link
@ObservedObject+ObservableObject+@Published @Observed+@ObjectLink
@StateObject ?
@Binding+projectedValue @Provide和@Consume
@Publisher @Watch
SwiftUI
@Binding官方定义:
A property wrapper type that can read and write a value managed by SwiftUI.
对被声明为 @Binding 的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递。
在传递数据时,我们在它前面加上美元符号 $。对一个由 @ 符号修饰的属性,在它前面使用 $ 所取得的值,被称为投影属性 (projection property)。有些 @ 属性,比如这里的 @State 和 @Binding,它们的投影属性就是自身所对应值的 Binding 类型。
The wrappedValue is: a value that is bound to something else.
What it does:
-1 gets/sets the value of the wrappedValue from some other source.
-2 when the bound-to value changes, it invalidates the View.
Projected value (i.e.$): a Binding to the Binding itself
数据源只有⼀个(source of the truth)的场景,就不需要⽤两个@State⽽⽤@Binding
1.struct MyView: View {
2. @State var myString = “Hello” // 1
3. var body: View {
4. OtherView(sharedText: $myString) // 2
5. }
6. }
7.struct OtherView: View {
8. @Binding var sharedText: string // 3
9. var body: View {
10. Text(sharedText) // 4
11. TextField(“shared”, text: $sharedText) // 5 _myString.projectValue.projectValue
12. }
13.}
ArkUI
@Prop
单向数据绑定
1.@Component
2.struct CountDownComponent {
3. @Prop count: number;
4. costOfOneAttempt: number = 1;
5.
6. build() {
7. Column() {
8. if (this.count > 0) {
9. Text(You have ${this.count} Nuggets left
)
10. } else {
11. Text(‘Game over!’)
12. }
13. // @Prop装饰的变量不会同步给父组件
14. Button(Try again
).onClick(() => {
15. this.count -= this.costOfOneAttempt;
16. })
17. }
18. }
19.}
20.
21.@Entry
22.@Component
23.struct ParentComponent {
24. @State countDownStartValue: number = 10;
25.
26. build() {
27. Column() {
28. Text(Grant ${this.countDownStartValue} nuggets to play.
)
29. // 父组件的数据源的修改会同步给子组件
30. Button(+1 - Nuggets in New Game
).onClick(() => {
31. this.countDownStartValue += 1;
32. })
33. // 父组件的修改会同步给子组件
34. Button(-1 - Nuggets in New Game
).onClick(() => {
35. this.countDownStartValue -= 1;
36. })
37.
38. CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
39. }
40. }
41.}
@Link
双向数据绑定
1.@Entry
2.@Component
3.struct Player {
4. @State isPlaying: boolean = false
5.
6. build() {
7. Column() {
8. PlayButton({ buttonPlaying: $isPlaying })
9. Text(Player is ${this.isPlaying ? '' : 'not'} playing
).fontSize(18)
10. Button(‘Parent:’ + this.isPlaying)
11. .margin(15)
12. .onClick(() => {
13. this.isPlaying = !this.isPlaying
14. })
15. }
16. }
17.}
18.
19.@Component
20.struct PlayButton {
21. @Link buttonPlaying: boolean
22.
23. build() {
24. Column() {
25. Button(this.buttonPlaying ? ‘pause’ : ‘play’)
26. .margin(20)
27. .onClick(() => {
28. this.buttonPlaying = !this.buttonPlaying
29. })
30. }
31. }
32.}
SwiftUI
@ObservedObject官方定义:
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
不持有数据,只是建立了绑定关系。
The wrappedValue is: anything that implements the Observable0bject protocol(ViewModels). What it does: invalidates the View when wrappedValue does objectWillChange.send().
Projected value (i.e. $): a Binding to the vars of the wrappedValue ViewModel.
如果说 @State 是全自动驾驶的话,ObservableObject 就是半自动,它需要一些额外的声明。ObservableObject 协议要求实现类型是 class,它只有一个需要实现的属性:objectWillChange。在数据将要发生改变时,这个属性用来向外进行 “广播”,它的订阅者 (一般是 View 相关的逻辑) 在收到通知后,对 View 进行刷新。
ObservableObject官方定义:
A type of object with a publisher that emits before the object has changed.
创建 ObservableObject 后,实际在 View 里使用时,我们需要将它声明为@ObservedObject。这也是一个属性包装,它负责通过订阅 objectWillChange 这个“广播”,将具体管理数据的 ObservableObject 和当前的 View 关联起来。
1.class aViewModel: ObservableObject {
2. func change {
3. objectWillChange.send()//可以发出通知
4. }
5. //或 @Published获取Publisher
6. @Published private var model: M = createM()
7.}
8.class view:View {
9. @ObservedObject let viewModel: aViewModel //不持有
10. var body: some View {
11. //…
12. }
13.}
和 @State 这种底层存储被 SwiftUI “全面接管” 的状态不同,@ObservedObject 只是在 View 和 Model 之间添加订阅关系,而不影响存储。因此,当 ContentView 中的状态发生变化,ContentView.body 被重新求值时,ScorePlate 就会被重新生成,其中的 model 也一同重新生成,导致了状态的“丢失”。运行代码,在 Xcode console 中可以看到每次点击 Toggle 按钮时都伴随着 Model.init 的输出。
1.struct ContentView: View {
2. @State private var showRealName = false
3. var body: some View {
4. VStack {
5. Button(“Toggle Name”) {
6. showRealName.toggle()
7. }
8. Text(“Current User: (showRealName ? “Wei Wang” : “onevcat”)”)
9. ScorePlate().padding(.top, 20)
10. }
11. }
12.}
13.
14.struct ScorePlate: View {
15.
16. @ObservedObject var model = Model()
17. @State private var niceScore = false
18.
19. var body: some View {
20. VStack {
21. Button(“+1”) {
22. if model.score > 3 {
23. niceScore = true
24. }
25. model.score += 1
26. }
27. Text(“Score: (model.score)”)
28. Text(“Nice? (niceScore ? “YES” : “NO”)”)
29. ScoreText(model: model).padding(.top, 20)
30. }
31. }
32.}
如上,如果点击按钮,model的属性会被重置。
1.struct ScorePlate: View {
2. @ObservedObject var model = Model()
3. //…
4.}
只要把 ScorePlate 中的 @ObservedObject 改成 @StateObject,状态不会被重置了。
1.struct ScorePlate: View {
2. // @ObservedObject var model = Model()
3. @StateObject var model = Model()
4.}
@Published官方定义:
A type that publishes a property marked with an attribute.
在 ObservableObject 中,对于每个对界面可能产生影响的属性,我们都可以,手动调用 objectWillChange.send()。如果在 model 中有很多属性,我们将需要为它们一一添加 willSet,这无疑是非常麻烦,而且全是重复的模板代码。实际上,如果我们省略掉自己声明的 objectWillChange,并把属性标记为@Published,编译器将会帮我们自动完成这件事情。
1.//等价于@Published var value = 0
2.var value = 0 {
3. willSet {
4. objectWillChange.send()
5. }
6.}
ObservableObject 协议的唯一要求是实现 objectWillChange,它是一个 publisher,会在对象
变更时发送事件。通过在 model 属性前面添加 @Published,框架会为我们创建一个objectWillChange 的实现,在每次这两个属性发生改变的时候发送事件。
@StateObject官方定义:
A property wrapper type that instantiates an observable object.
@StateObject is a “source of truth”,也就是说可以直接赋值: @StateObject var foo = SomeObservableObject()
如果⽤在View⾥,⽣命周期与View⼀致。
1.struct LibraryView: View {
2. @StateObject var book = Book()
3.
4. var body: some View {
5. BookView(book: book)
6. }
7.}
8.
9.VStack {
10. LibraryView()
11. LibraryView()
12.}
持有数据,@StateObject 就是 @State 的升级版:@State 是针对 struct 状态所创建的存储,@StateObject 则是针对 ObservableObject class 的存储。它保证这个 class 实例不会随着 View 被重新创建。
ArkUI
@Observed
@ObjectLink
上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
@Observed用于类,@ObjectLink用于变量。
1.@Observed
2.class ClassA {
3. public name: string
4. public c: number
5. public id: number
6.
7. constructor(c: number, name: string = ‘OK’) {
8. this.name = name
9. this.c = c
10. this.id = nextID++
11. }
12.}
13.
14.@Component
15.struct ViewA {
16. label: string = ‘ViewA1’
17. @ObjectLink a: ClassA
18.
19. build() {
20. Row() {
21. Button(ViewA [${this.label}] this.a.c= ${this.a.c} +1
)
22. .onClick(() => {
23. this.a.c += 1
24. })
25. }.margin({ top: 10 })
26. }
27.}
@Provide和@Consume
@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。
在下面的示例是与后代组件双向同步状态@Provide和@Consume场景。当分别点击CompA和CompD组件内Button时,reviewVotes 的更改会双向同步在CompA和CompD中。
1.@Component
2.struct CompD {
3. // @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量
4. @Consume reviewVotes: number;
5.
6. build() {
7. Column() {
8. Text(reviewVotes(${this.reviewVotes})
)
9. Button(reviewVotes(${this.reviewVotes}), give +1
)
10. .onClick(() => this.reviewVotes += 1)
11. }
12. .width(‘50%’)
13. }
14.}
15.
16.@Component
17.struct CompC {
18. build() {
19. Row({ space: 5 }) {
20. CompD()
21. CompD()
22. }
23. }
24.}
25.
26.@Component
27.struct CompB {
28. build() {
29. CompC()
30. }
31.}
32.
33.@Entry
34.@Component
35.struct CompA {
36. // @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件
37. @Provide reviewVotes: number = 0;
38.
39. build() {
40. Column() {
41. Button(reviewVotes(${this.reviewVotes}), give +1
)
42. .onClick(() => this.reviewVotes += 1)
43. CompB()
44. }
45. }
46.}
@Watch
给状态变量增加一个@Watch装饰器,通过@Watch注册一个回调方法onChanged, 当状态变量count被改变时, 触发onChanged回调。
1.@Entry
2.@Component
3.struct CompA {
4. @State @Watch(‘onBasketUpdated’) shopBasket: Array = [7, 12, 47, 3]
5. @State totalPurchase: number = 0
6. @State addPurchase: number = 0
7.
8. aboutToAppear() {
9. this.updateTotal()
10. }
11.
12. updateTotal(): void {
13. let sum = 0;
14. this.shopBasket.forEach((i) => {
15. sum += i
16. })
17. // 计算新的购物篮总价值,如果超过100,则适用折扣
18. this.totalPurchase = (sum < 100) ? sum : 0.9 * sum
19. return this.totalPurchase
20. }
21.
22. // shopBasket更改时触发该方法
23. onBasketUpdated(propName: string): void {
24. this.updateTotal()
25. }
26.
27. build() {
28. Column() {
29. Button('add to basket ’ + this.addPurchase)
30. .margin(15)
31. .onClick(() => {
32. this.addPurchase = Math.round(100 * Math.random())
33. this.shopBasket.push(this.addPurchase)
34. })
35. Text(${this.totalPurchase}
)
36. .fontSize(30)
37. }
38. }
39.}
内置组件双向同步一种特殊场景的 @ S t a t e + @ L i n k ? 内置组件双向同步 一种特殊场景的@State+@Link? 内置组件双向同步一种特殊场景的@State+@Link?运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。
1.// xxx.ets
2.@Entry
3.@Component
4.struct bindPopupPage {
5. @State customPopup: boolean = false;
6.
7. build() {
8. Column() {
9. Button(‘Popup’)
10. .margin(20)
11. .onClick(() => {
12. this.customPopup = !this.customPopup
13. })
14. .bindPopup($$this.customPopup, {
15. message: ‘showPopup’
16. })
17. }
18. }
19.}
-1 当前 支持基础类型变量,以及 @ S t a t e 、 @ L i n k 和 @ P r o p 装饰的变量。 − 2 当前 支持基础类型变量,以及@State、@Link和@Prop装饰的变量。 -2 当前 支持基础类型变量,以及@State、@Link和@Prop装饰的变量。−2当前仅支持bindPopup属性方法的show参数,Radio组件的checked属性,Refresh组件的refreshing参数。
-3 $$绑定的变量变化时,会触发UI的同步刷新。
小结
SwiftUI ArkUI
@State+@Binding @State+@Prop+@Link
@ObservedObject+ObservableObject+@Published @Observed+@ObjectLink
@StateObject ?
@Binding+projectedValue @Provide和@Consume
@Publisher @Watch
页面级状态管理SwfitUI和ArkUI的思想基本一致,相比之下,ArkUI多了一个单向数据绑定(@Prop)以及一个跨级数据绑定(@Provide和@Consume)。对于第二层属性为class的情况,@ObjectLink同样不持有数据,但SwiftUI需要额外添加@Published来发送更新,而ArkUI则是对@Observed修饰的属性全量通知(class级别)。
使用的应用场景概括如下,@State用于单个struct中数据绑定,@Prop和@Link用于struct和class之间数据绑定,@Provide和@Consume用于多层struct之间数据传递,@Watch可以额外提供一个数据监听函数。
应用级变量的状态管理
SwiftUI ArkUI
@EnvironmentObject @LocalStorageLink+@LocalStorageProp
应用级变量用于两个视图之间共享数据。
SwiftUI
@EnvironmentObject
将对象放置到环境中,以便任何子视图都可以自动访问它。
1.struct ContentView: View {
2. let user = User()
3.
4. var body: some View {
5. VStack {
6. EditView().environmentObject(user)
7. DisplayView().environmentObject(user)
8. }
9. }
10.}
在环境中查找User实例,并将其找到的内容放入user属性中。
1.struct EditView: View {
2. @EnvironmentObject var user: User
3.
4. var body: some View {
5. TextField(“Name”, text: $user.name)
6. }
7.}
8.
9.struct DisplayView: View {
10. @EnvironmentObject var user: User
11.
12. var body: some View {
13. Text(user.name)
14. }
15.}
自动继承环境也可以这么写:
1.VStack {
2. EditView()
3. DisplayView()
4.}
5…environmentObject(user)
可能一开始你会认为 @EnvironmentObject 和单例很像:只要我们在View 的层级上,不论何处都可以访问到这个环境对象。看似这会带来状态管理上的困难和混乱,但是 Swift 提供了清晰的状态变更和界面刷新的循环,如果我们能选择正确的设计和架构模式,完全可以避免这种风险。使用 @EnvironmentObject 带来很大的便捷性,也避免了大量不必要的属性传递,这会为之后代码变更带来更多的好处。
@Environment
与 @EnvironmentObject 完全不是同⼀个东⻄。@Environment 就是 View 所在环境的各种环境变量信息的集合。你可以通过键路径 (key path) 对其读写。
它主要是用来处理一些当前的系统设置的,比如说语言、时区、黑暗模式。
在使用过程中一个很大的不同是,@Environment(_ keyPath:)需要指定一个类型为KeyPath的参数,而这个参数大多数情况下我们都是使用的EnvironmentValues中已经定义好的,比如managedObjectContext/locale等。
The WrappedValue is: the value of some var in EnvironmentValues.
What it does: gets a value of Some var in EnvironmentValues.
Projected value (i.e. $): none.
1.// App.swift
2…
3.let persistenceController = PersistenceController.shared
4…
5.var body: some Scene {
6. WindowGroup {
7. ContentView()
8. .environment(.managedObjectContext, persistenceController.container.viewContext)
9. }
10.}
11…
12.
13.// ContentView.swift
14…
15.@Environment(.managedObjectContext) private var viewContext
16…
17.let newItem = Item(context: viewContext)
18.newItem.platform = 1
19…
在这个例子中,我们在App.swift中创建了persistenceController并通过调用.environment把值传递给了ContentView。ContentView中使用@Environment(.managedObjectContext) private var viewContext获取到了这个环境变量。
1.struct ContentView: View {
2. // colorScheme values: .light, .dark
3. @Enviroment(.colorScheme) var colorScheme
4.
5. var body: some View {
6. Text(“Hello, World”)
7. .foregroundColor(colorScheme = .light ? .yellow : .blue)
8. }
9.}
根据 Light 和 Dark 两种系统颜色模式来调整 app 界面的配色是很常见的需求。方法很简单,通过 Environment.colorScheme 获取当前系统环境的颜色方案就行了:
持久化存储
File system(FileManager)
Sqlite/CoreData
iCloud: 根据上⾯两种格式存储
CloutKit: a database in the cloud (network)
UserDefaults
ArkUI
AppStorage
AppStorage是应用程序中的单例对象,由UI框架在应用程序启动时创建,在应用程序退出时销毁。
相当于一个全局的map?
生命周期=app生命周期?
@StorageLink
双向数据绑定
1.// 创建新实例并使用给定对象初始化
2.let storage = new LocalStorage({ ‘PropA’: 47 });
3.
4.@Component
5.struct Child {
6. // @LocalStorageLink变量装饰器与LocalStorage中的’ProA’属性建立双向绑定
7. @LocalStorageLink(‘PropA’) storLink2: number = 1;
8.
9. build() {
10. Button(Child from LocalStorage ${this.storLink2}
)
11. // 更改将同步至LocalStorage中的’ProA’以及Parent.storLink1
12. .onClick(() => this.storLink2 += 1)
13. }
14.}
15.// 使LocalStorage可从@Component组件访问
16.@Entry(storage)
17.@Component
18.struct CompA {
19. // @LocalStorageLink变量装饰器与LocalStorage中的’ProA’属性建立双向绑定
20. @LocalStorageLink(‘PropA’) storLink1: number = 1;
21.
22. build() {
23. Column({ space: 15 }) {
24. Button(Parent from LocalStorage ${this.storLink1}
) // initial value from LocalStorage will be 47, because ‘PropA’ initialized already
25. .onClick(() => this.storLink1 += 1)
26. // @Component子组件自动获得对CompA LocalStorage实例的访问权限。
27. Child()
28. }
29. }
30.}
1.在UI组件中对@StorageLink的状态变量所做的更改将同步到AppStorage
2.AppStorage中属性值的更改会导致绑定该状态变量的UI组件进行状态更新。
@StorageProp
单向数据绑定
1.@StorageProp(‘languageCode’) languageCode: string = ‘en’
3.AppStorage中属性值的更改会导致绑定该状态变量的UI组件进行状态更新。
Get
1.AppStorage.Get(‘languageCode’)
Set
1.AppStorage.Set(‘languageCode’, ‘en’)
LocalStorage
生命周期=Ability生命周期
@LocalStorageLink
双向数据绑定
@LocalStorageProp
单向数据绑定
PersistentStorage
AppStorage的持久化存储版,不加@
1.PersistentStorage.PersistProp(‘highScore’, ‘0’)
生命周期=只要app不删除下次打开还在
Environment
应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态数据。
1.Environment.EnvProp(‘accessibilityEnabled’, ‘default’)
2.var enable = AppStorage.Get(‘accessibilityEnabled’)
绑定到AppStorage上,通过AppStorage的get获取。
是AppStorage的一部分?和AppStorage的不同在数据不可变。
小结
页面级UI状态存储在使用上SwiftUI和ArkUI只有语法上的区别,思想上SwiftUI像是“从外注入”属性,而ArkUI更像是声明全局变量然后再使用。ArkUI通过Key/Value映射来实现,需要注意Key值一致性问题,防止映射错误。
组件&合成
SwiftUI ArkUI
@ViewBuilder @Builder+@BuilderParam
组件合成的作用是为了将归并,一次编写,多出调用,既可以减少代码量,也可以降低复制相同代码的错误率。
SwiftUI
@ViewBuilde
基本的SwiftUI元素和组件已经没办法满足我们项目复杂需求或者定制化的要求时,我们要DIY一下扩展性更强的View。
@ViewBuilder本质上是一个结构体,并且被@resultBuilder注解,也就是说ViewBuilder是一个reult builder(结果建造者)类型了。
ViewBuilder结构体有11个名为buildBlock的函数,分别接收从0到10个View类型的参数,因此在SwiftUI中一个接收@ViewBuilder类型参数的视图容器最多能接收10个子视图,如果不能满足需求可以通过拆分来增加子视图的个数。
1.import SwiftUI
2.
3.struct NotificationView
4. let content: Content
5.
6. init(@ViewBuilder content: () -> Content) {
7. self.content = content()
8. }
9.
10. var body: some View {
11. content
12. .padding()
13. .background(Color(.tertiarySystemBackground))
14. .cornerRadius(16)
15. .transition(.move(edge: .top))
16. .animation(.spring())
17. }
18.}
19.
20.struct ContentView: View {
21. @State private var notificationShown = false
22.
23. var body: some View {
24. VStack {
25. if self.notificationShown {
26. NotificationView {
27. Text(“notification”)
28. }
29. }
30. Button(“toggle”) {
31. self.notificationShown.toggle()
32. }
33. }
34. }
35.}
ArkUI
@Builder
ArkUI还提供了一种更轻量的UI元素复用机制@Builder,@Builder所装饰的函数遵循build()函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。
为了简化语言,我们将@Builder装饰的函数也称为“自定义构建函数”。
按引用传递参数:
1.@Builder function ABuilder(KaTeX parse error: Can't use function '$' in math mode at position 78: …arByReference: $̲{.paramA1} `)
4. }
5.}
6.@Entry
7.@Component
8.struct Parent {
9. @State label: string = ‘Hello’;
10. build() {
11. Column() {
12. // 在Parent组件中调用ABuilder的时候,将this.label引用传递给ABuilder
13. ABuilder({ paramA1: this.label })
14. Button(‘Click me’).onClick(() => {
15. // 点击“Click me”后,UI从“Hello”刷新为“ArkUI”
16. this.label = ‘ArkUI’;
17. })
18. }
19. }
20.}
按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。ArkUI提供$$作为按引用传递参数的范式。
@BuilderParam:引用@Builder函数
当开发者创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,ArkUI引入了@BuilderParam装饰器,@BuilderParam用来装饰指向@Builder方法的变量,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。
1.@Component
2.struct Child {
3. label: string = Child
4. @BuilderParam aBuilder0: () => void;
5.
6. build() {
7. Column() {
8. this.aBuilder0()
9. }
10. }
11.}
12.
13.@Entry
14.@Component
15.struct Parent {
16. label: string = Parent
17.
18. @Builder componentBuilder() {
19. Text(${this.label}
)
20. }
21.
22. build() {
23. Column() {
24. this.componentBuilder()
25. Child({ aBuilder0: this.componentBuilder })
26. }
27. }
28.}
示例中,Parent组件在调用this.componentBuilder()时,this.label指向其所属组件,即“Parent”。@Builder componentBuilder()传给子组件@BuilderParam aBuilder0,在Child组件中调用this.aBuilder0()时,this.label指向在Child的label,即“Child”。
小结:
这个功能在思想上ArkUI和SwiftUI一致,允许我们在视图中封装和重用任何逻辑,利用自定义的函数能全局改变UI的控件样式。简单说就是打包一堆代码,而相比ArkUI多了一种@BuilderParam特性,是一种特殊场景的扩展,让开发者有更多的操作空间。
生命周期
SwiftUI
SwiftUI 试图淡化视图生命周期的概念,在大多数场景下确实实现了它的设计目标。开发者即使不了解文本上述的内容,也可以让 SwiftUI 的代码在日常中发挥出不错的效率。
1.struct ContentView: View {
2. var body: some View {
3. NavigationView {
4. VStack {
5. NavigationLink(destination: DetailView()) {
6. Text(“Hello World”)
7. }
8. }
9. }.onAppear {
10. print(“ContentView appeared!”)
11. }.onDisappear {
12. print(“ContentView disappeared!”)
13. }
14. }
15.}
16.
17.struct DetailView: View {
18. var body: some View {
19. VStack {
20. Text(“Second View”)
21. }.onAppear {
22. print(“DetailView appeared!”)
23. }.onDisappear {
24. print(“DetailView disappeared!”)
25. }
26. }
27.}
SwiftUI在body的闭包只执行一次,等价于UIKit的viewDidLoad()方法。UIKit的 viewDidAppear() 和 viewDidDisappear() 等价于SwiftUI的方法onAppear() 和 onDisappear()。
SwiftUI还可以接收应用生命周期改变的通知:
1.VStack {
2. Text(“Hello, World!”)
3. .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
4. print(“即将进入后台”)
5. }.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
6. print(“即将进入前台”)
7. }.onReceive(NotificationCenter.default.publisher(for: UIApplication.userDidTakeScreenshotNotification)) { _ in
8. print(“用户截屏了”)
9. }.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
10. print(“已经进入后台”)
11. }.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
12. print(“已经进入前台”)
13. }.onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in
14. print(“程序将要销毁”)
15. }
16.
通过检测您的应用何时移至后台(即用户何时返回主屏幕),何时回到前台,用户何时截取屏幕截图等等。这些全部由 Notification Center 提供支持。
ArkUI
区分了自定义组件和页面:
-1 自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用。
-2 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。
页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:
-1 onPageShow:页面每次显示时触发。
-2 onPageHide:页面每次隐藏时触发一次。
-3 onBackPress:当用户点击返回按钮时触发。
组件生命周期,即一般用@Component装饰的自定义组件,提供以下生命周期接口:
-1 aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
-2 aboutToDisappear:在自定义组件即将析构销毁时执行。
1.// Index.ets
2.import router from ‘@ohos.router’;
3.
4.@Entry
5.@Component
6.struct MyComponent {
7. @State showChild: boolean = true;
8.
9. // 只有被@Entry装饰的组件才可以调用页面的生命周期
10. onPageShow() {
11. console.info(‘Index onPageShow’);
12. }
13. // 只有被@Entry装饰的组件才可以调用页面的生命周期
14. onPageHide() {
15. console.info(‘Index onPageHide’);
16. }
17.
18. // 只有被@Entry装饰的组件才可以调用页面的生命周期
19. onBackPress() {
20. console.info(‘Index onBackPress’);
21. }
22.
23. // 组件生命周期
24. aboutToAppear() {
25. console.info(‘MyComponent aboutToAppear’);
26. }
27.
28. // 组件生命周期
29. aboutToDisappear() {
30. console.info(‘MyComponent aboutToDisappear’);
31. }
32.
33. build() {
34. Column() {
35. // this.showChild为true,创建Child子组件,执行Child aboutToAppear
36. if (this.showChild) {
37. Child()
38. }
39. // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
40. Button(‘create or delete Child’).onClick(() => {
41. this.showChild = false;
42. })
43. // push到Page2页面,执行onPageHide
44. Button(‘push to next page’)
45. .onClick(() => {
46. router.pushUrl({ url: ‘pages/Page2’ });
47. })
48. }
49.
50. }
51.}
52.
53.@Component
54.struct Child {
55. @State title: string = ‘Hello World’;
56. // 组件生命周期
57. aboutToDisappear() {
58. console.info(‘[lifeCycle] Child aboutToDisappear’)
59. }
60. // 组件生命周期
61. aboutToAppear() {
62. console.info(‘[lifeCycle] Child aboutToAppear’)
63. }
64.
65. build() {
66. Text(this.title).fontSize(50).onClick(() => {
67. this.title = ‘Hello ArkUI’;
68. })
69. }
70.}
1.
相比SwiftUI,ArkUI的生命周期分为两类,其中页面级生命周期更像iOS开发以前的ViewController模式的生命周期。而组件级的生命周期更像SwiftUI目前的设计。
抽象
SwiftUI ArkUI
ViewModifier @Styles
SwiftUI
ViewModifier
如果App中的主要按钮都使用一种样式,我们就可以把修饰符部分抽离出来,构建一个ViewModifier视图修饰器。
1.struct MainTitle: ViewModifier {
2. func body(content: Content) -> some View {
3. content
4. .font(.system(size: 17))
5. .foregroundColor(.white)
6. .padding()
7. .background(Color.blue)
8. .clipShape(Capsule())
9. }
10.}
11.
12.struct ContentView: View {
13. var body: some View {
14. Text(“abc”)
15. .modifier(MainTitle())
16. }
17.}
上述代码中,我们将原来给Text文字按钮的修饰符抽离构建成一个新的视图修饰器MainTitle,我们的文字按钮修饰符将修饰content。
如果我们要更加优雅点,想要像修饰符一样调用ViewModifier视图修饰器,也可以做一些拓展:
1.extension View {
2. func mainTitle() -> some View {
3. self.modifier(MainTitle())
4. }
5.}
6.
7.struct ContentView: View {
8. var body: some View {
9. Text(“abc”)
10. .mainTitle()
11. }
12.}
这样我们给Text添加修饰符时,就可以直接使用mainTitle视图。
ArkUI
@Styles
如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用的装饰器@Styles。
@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。
1.// 定义在全局的@Styles封装的样式
2.@Styles function globalFancy () {
3. .width(150)
4. .height(100)
5. .backgroundColor(Color.Pink)
6.}
7.
8.@Entry
9.@Component
10.struct FancyUse {
11. @State heightVlaue: number = 100
12. // 定义在组件内的@Styles封装的样式
13. @Styles fancy() {
14. .width(200)
15. .height(this.heightVlaue)
16. .backgroundColor(Color.Yellow)
17. .onClick(() => {
18. this.heightVlaue = 200
19. })
20. }
21.
22. build() {
23. Column({ space: 10 }) {
24. // 使用全局的@Styles封装的样式
25. Text(‘FancyA’)
26. .globalFancy ()
27. .fontSize(30)
28. // 使用全局的@Styles封装的样式
29. Text(‘FancyB’)
30. .fancy()
31. .fontSize(30)
32. }
33. }
34.}
示例中演示了组件内@Styles和全局@Styles的用法。
扩展
SwiftUI ArkUI
Extention @Extend
enum stateStyles
SwiftUI
Swift的扩展和OC的分类类似,可以增强已有类的功能。通过分类,我们可以在不破坏原有类的结构的前提下,对原有类进行模块化的扩展。但是在swift中没有分类这种写法了。相对应的是swift中只有扩展(Extensions)。扩展就是向一个已有的类、结构体、枚举类型或者协议类型添加新功能(functionality)。扩展和 Objective-C 中的分类类似。(不过与 Objective-C 不同的是,Swift 的扩展没有名字。)
1.extension String {
2. var md5:String {
3. let utf8 = cString(using: .utf8)
4. var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
5. CC_MD5(utf8, CC_LONG(utf8!.count - 1), &digest)
6. return digest.reduce(“”) { $0 + String(format:“%02X”, $1) }
7. }
8.}
使用时只需用extension修饰类即可。
ArkUI
@Extend,用于扩展原生组件样式。
-1 和@Styles不同,@Extend仅支持定义在全局,不支持在组件内部定义。
-2 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法。
1.@Extend(Text) function fancyText(weightValue: number, color: Color) {
2. .fontStyle(FontStyle.Italic)
3. .fontWeight(weightValue)
4. .backgroundColor(color)
5.}
6.
7.@Entry
8.@Component
9.struct FancyUse {
10. @State label: string = ‘Hello World’
11.
12. build() {
13. Row({ space: 10 }) {
14. Text(${this.label}
)
15. .fancyText(100, Color.Blue)
16. Text(${this.label}
)
17. .fancyText(200, Color.Pink)
18. Text(${this.label}
)
19. .fancyText(200, Color.Orange)
20. }.margin(‘20%’)
21. }
22.}
通过@Extend组合样式后,使得代码更加简洁,增强可读性。
stateStyles
@Styles和@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式(又称为:多态样式)。
stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下四种状态:
-1 focused:获焦态。
-2 normal:正常态。
-3 pressed:按压态。
-4 disabled:不可用态。
小结
相比SwiftUI,ArkUI扩展方法的思想一致,是对一个类方法的扩展,但对于指定私有属性的功能SwiftUI以在ViewModifier 中体现,SwiftUI的extension还可以通过关联对象的方式扩展属性。
对于状态扩展,SwiftUI通过自定义枚举的形式更为灵活,ArkUI高度封装了固定的多态样式,对于初学者理解更友好。这两者提供的特性对于管理组件状态非常有用。
设计模式
设计模式是一种组织代码的思想,它体现了你构建应用的方式,也展示了管理代码的结构。对于一个几行代码的demo来说,开发模式可能不那么重要,你可以把他们写在一起。但是对于一个复杂的应用,多人协同开发时,大家约定一种架构模型非常重要,不仅可以提高开发效率,还有助于BUG定位,责任分离,是必须要思考的环节。
SwiftUI
MVVM模式
MVVM(Model-View-ViewModel)并非一种框架,而是一种架构模式,一种思想,一种组织和管理代码的方法。它本质上就是 MVC(Model-View- Controller)的一种改进版。
ViewModel 总是 class 而不是 struct。
在 MVVM 架构中 View 和 Model 不能直接通信,必须通过 ViewModel。ViewModel 是 MVVM 的核心,它通常要实现一个观察者,当 Model 数据发生变化时 ViewModel 能够监听并通知到对应的 View 做 UI 更新,反之当用户操作 View 时 ViewModel 也能获取到数据的变化并通知 Model 数据做出对应的更新操作。这就是 MVVM 中数据的双向绑定。
1.struct ContentView: View {
2.
3. @ObservedObject var viewModel: ViewModel = ViewModel()
4.
5. var body: some View {
6. List{
7. ForEach(viewModel.countries) { country in
8. Text(country.name)
9. }
10. .onDelete{ index in
11. self.viewModel.removeCountry(index.first!)
12. }
13. }.onAppear {
14. self.viewModel.loadCountries()
15. }
16. }
17.}
18.
19.class ViewModel: ObservableObject {
20.
21. @Published private(set) var countries: [Country] = []
22.
23. func loadCountries() {
24. self.countries = [Country(name: “中国”), Country(name: “日本”)]
25. }
26.
27. func removeCountry(_ index: Int) {
28. self.countries.remove(at: index)
29. }
30.}
Model—>View
-1 List(View)显示时onAppear调用,紧接着调用 ViewModel 中的loadCountries()。
-2 ViewModel 中的loadCountries()构造/获取数据并转化为 Model。
-3 ViewModel 通过@Published修饰的属性发出数据变化通知。
-4 View 中的@ObservedObject收到通知后驱动 UI 更新。
View—>Model
-1 List(View)侧滑时,进行删除操作,调用 ViewModel 中的removeCountry()。
-2 ViewModel 中的removeCountry()操作数据 Model。
-3 ViewModel 通过@Published修饰的属性发出数据变化通知。
-4 View 中的@ObservedObject收到通知后驱动 UI 更新。
ArkUI
文档暂无相关章节讲设计模式。
在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。在进行 UI 界面开发时,通常不是简单的将系统组件进行组合使用,而是需要考虑代码可复用性、业务逻辑与UI分离,后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。
没有引入MVVM的概念,从文档看,修改模型方法一般写在component同一个文件内。
(https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/quick-start/arkts-create-custom-components.md)但是修改model也是class而不是struct的思想是和SwiftUI一致的。
@Component:
@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。
自定义组件具有以下特点:
-1 可组合:允许开发者组合使用系统组件、及其属性和方法。
-2 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
-3 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。
1.// 自定义controller
2.export class MyComponentController {
3. item: MyComponent = null;
4.
5. setItem(item: MyComponent) {
6. this.item = item;
7. }
8.
9. changeText(value: string) {
10. this.item.value = value;
11. }
12.}
13.
14.// 自定义组件
15.@Component
16.export default struct MyComponent {
17. public controller: MyComponentController = null;
18. @State value: string = ‘Hello World’;
19.
20. build() {
21. Column() {
22. Text(this.value)
23. .fontSize(50)
24. }
25. }
26.
27. aboutToAppear() {
28. if (this.controller)
29. this.controller.setItem(this); // 绑定controller
30. }
31.}
32.
33.// 使用处逻辑
34.@Entry
35.@Component
36.struct StyleExample {
37. controller = new MyComponentController();
38.
39. build() {
40. Column() {
41. MyComponent({ controller: this.controller })
42. }
43. .onClick(() => {
44. this.controller.changeText(‘Text’);
45. })
46. }
47.}
在上面的示例中:
-1 通过子组件MyComponent的aboutToAppear方法,把当前的this指针传递给MyComponentController的item成员变量。
-2 在StyleExample父组件中持有controller实例,调用controller的changeText方法,即相当于通过controller持有的MyComponent子组件的this指针,改变MyComponent的状态变量value的值。
-3 通过controller的封装,MyComponent对外暴露了changeText的接口,所有持有controller的实例都可以通过调用changeText接口,改变MyComponent的状态变量value的值。
总结
同为声明式语言,SwiftUI 和ArkUI在设计和使用上非常类似,对于SwiftUI 开发者可以快速过度到ArkUI的开发,特别注意两者的语法区别,如果掌握额外特性还可以有助于高效开发。
SwiftUI
随着经验的积累,你会逐渐形成对于某个场景下应该使用哪种方式来管理数据和状态的直觉。在此之前,如果你纠结于选择使用哪种方式的话,从 ObservableObject开始入手会是一个相对好的选择:如果发现状态可以被限制在同一个 View 层级中,则改用 @State;如果发现状态需要大批量共享,则改用 @EnvironmentObject。
ArkUI
-1 @Provide和@Consume:与后代组件双向同步,隐性传递功能,跨组件传递无需逐级依次传递。@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。
-2 比SwiftUI多一种单向数据绑定。
-3 比SwiftUI多一种监听函数@Watch
SwiftUI开发设计模式引入MVVM的概念,ArkUI是Components和Application,Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。
特别注意两者的区别总结如下:
SwiftUI ArkUI
@State @State+@Prop
ArkUI多一个单向绑定,父组件单向传递给子组件
@ObservedObject+ObservableObject+@Published @Observed+@ObjectLink
SwiftUI需要额外添加@Published才会发生change通知,ArkUI则会对@Obseved修饰的整个class的属性添加change通知订阅。注意@ObservedObject和@ObjectLink都不创建实例,而是添加一个观察者,绑定change通知。
@StateObject ?
SwiftUI对于上诉不能创建实例的观察者情况新增了一个@StateObject,可以持有对象。
@Binding+projectedValue @Provide和@Consume
SwiftUI对于父子继承的传递只能通过projectValue逐级传递,而ArkUI提供了@Provide,可以不在中间类写代码,直接传递到最下层。
@Publisher @Watch
在SwiftUI中,Publisher帮助你创建objectWillChange.send()发送通知,在ArkUI中Watch可以主动添加一个函数,在属性改变时调用。
@EnvironmentObject @LocalStorageLink+@LocalStorageProp
都是跨页面调用,SwiftUI从外部注入,ArkUI在内部声明Key-Value,并且有单向绑定,还可以通过AppStorage进行app级别的共享。
@ViewBuilder @Builder+@BuilderParam
这个功能都是将一堆UI集合抽象成方法,为了可以多处调用。ArkUI额外增加了@BuilderParam,类似装饰模式,可以在原方法基础上修改某个属性以满足特定场景需要。
Extention @Extend
都是对原有类的扩展,ArkUI只可添加方法扩展,不能添加属性。
enum stateStyles
多种状态的管理。SwiftUI可以添加属性关联,可以为enum添加属性。ArkUI的stateStyles提供固定的状态(focused、normal、pressed、disabled),类似于css伪类。
Xxx
问题和缺陷
SwiftUI ArkUI
版本适配
ScrollView 性能问题
手势冲突
SwiftUI
SwiftUI版本的差异太大
如:
-1 ZStack它的大小iOS 14就是最大的frame而iOS 13就不是
-2 有的文字14上就正常,13上就截断
-3 iOS 13上navigation link点击了返回确又进入界面然后在返回
ScrollView 性能问题
日历应用中需要无限滚动功能,在 SwiftUI 里执行这个操作相对简单,但只能平滑向下滚动,因为尝试按需加载项目时向上滚动会导致明显的抖动。
但 SwiftUI 的 ScrollView 还有其他一些痛点,其根源在于它无法优雅地处理冲突手势,也就是说滚动可能会因为干扰手势而中断,反之亦然。
没法绝对的使用SwiftUI,最终总会有一些功能需要借助UIKit。适合开发一些中小型app。