SwiftUI @State, @Binding, @StateObject, @ObservedObject, @Observable 等的区别

1.@State@Binding 的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct ContentView: View {

@State var item: Item = Item(number: 1)

var body: some View {

VStack {
Text("\(item.number)")
.padding()
Divider()
Button("NUMBER + 1") {
item.number += 1
}
.padding()
Divider()
SecView(item: item)
.padding()
}
}
}

struct SecView: View {

@State var item: Item

var body: some View {
Text("\(item.number)")
}
}

struct Item: Identifiable {
let id: UUID = UUID()
var number: Int
}

这段代码,我们需要在点击按钮时,SecView 中的数字同步 ContentView。但事实是 SecView 中的数字一直保持为 1。

此时,只要将 SecView 中的 @State 改为 @Binding 即可:

1
2
3
4
5
6
7
8
struct SecView: View {

@Binding var item: Item

var body: some View {
Text("\(item.number)")
}
}

2.@ObservableObject, @StateObject 大学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
struct ContentView: View {

@ObservedObject var itemManager: ItemManager = ItemManager(items: [
Item(number: 1),
Item(number: 2)
])

var body: some View {

VStack {
ForEach(itemManager.items) { item in
HStack{
Text("\(item.number)")
Button("NUMBER + 1") {
itemManager.add(id: item.id)
}
}
}

Button("Create Item") {
itemManager.createItem()
}

Divider()

ForEach(itemManager.items) { item in
SecView(item: item)
}
}
}
}

struct SecView: View {

@State var item: Item

var body: some View {
Text("\(item.number)")
}
}

struct Item: Identifiable {
let id: UUID = UUID()
var number: Int
init(number: Int) {
self.number = number
}

mutating func add() {
self.number += 1
}
}

class ItemManager: ObservableObject {
@Published var items: [Item]
init(items: [Item]) {
self.items = items
}

func add(id: UUID) {
if let index = items.firstIndex(where: { $0.id == id }) {
items[index].add()
}
}

func createItem() {
items.append(Item(number: 3))
}
}

这段代码,我们需要

  1. 点击「NUMBER + 1」按钮,第一个 ForEach 对应的数字 +1;
  2. 同时,第二个 ForEach 中对应 SecView 的数字也 +1;
  3. 点击「Create Item」按钮,2 个 ForEach 都会新增 Item。

但上述代码只能实现 1、3,SecView 中的数字不会变化。

尝试一:将 @ObservedObject 调整为 @StateObject (失败)

1
2
3
4
@ObservedObject var itemManager: ItemManager = ItemManager(items: [
Item(number: 1),
Item(number: 2)
])

@ObservedObject@StateObject 在本例中没有区别。

@StateObject@ObservedObject 的区别

@StateObject@ObservedObject 基础作用,这里不再解释,可看:

  1. 探讨 SwiftUI 中的关键属性包装器:@State、@Binding、@StateObject、@ObservedObject、@EnvironmentObject 和 @Environment
  2. SwiftUI 数据绑定详解:ObservableObject、@Published 与 @StateObject

之前我遇到过在使用 @ObservedObject 声明 Model 后,随着 View 的刷新,Model 的数组会被清空。

具体来说可以这样表述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class People: ObservableObject {
@Published var age = 0
}

struct ObservedView: View {
@ObservedObject var people = People()
var body: some View {
VStack{
Text("\(people.age)")
Button(action: {people.age = people.age + 1}) {
Text("update (@ObservedObject)")
}
}
}
}

struct StateView: View {
@StateObject var people = People()
var body: some View {
VStack{
Text("\(people.age)")
Button(action: {people.age = people.age + 1}) {
Text("update (@StateObject)")
}
}
}
}

struct ContentView: View {
@State var count = 0
var body: some View {
VStack(spacing: 50){
VStack{
Text("\(count)")
Button(action: {count = count + 1} ) {
Text("update")
}
}
ObservedView()
StateView()
}
}
}

参考:#10 @ObservedObject的使用

在点击 ContentView 的 Button 后,ObservedView 的计数会重置,而 StateView 的计数并不会重置。等于是当更新 ContentView 的时候,原本在 ObservedView 的 people 对象又被重新生成了一次。

简单说,@ObservedObject 不管存储,会随着 View 的创建被多次创建。而 @StateObject 保证对象只会被创建一次。因此,如果是在 View 里自行创建的 ObservableObject model 对象,大概率来说使用 @StateObject会是更正确的选择。@StateObject 基本上来说就是一个针对 class 的 @State 升级版。

对于 View 自己创建的 ObservableObject 状态对象来说,极大概率你可能需要使用新的 @StateObject 来让它的存储和生命周期更合理:

具体可参考:@StateObject 和 @ObservedObject 的区别和使用

尝试二:将 Item 调整为 Class (成功)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
struct ContentView: View {

@ObservedObject var itemManager: ItemManager = ItemManager(items: [
Item(number: 1),
Item(number: 2)
])

var body: some View {

VStack {
ForEach(itemManager.items) { item in
HStack{
Text("\(item.number)")
Button("NUMBER + 1") {
itemManager.add(id: item.id)
}
}
}

Button("Create Item") {
itemManager.createItem()
}

Divider()

ForEach(itemManager.items) { item in
SecView(item: item)
}
}
}
}

struct SecView: View {

// @State var item: Item
@ObservedObject var item: Item // HERE!

var body: some View {
Text("\(item.number)")
}
}

class Item: Identifiable, ObservableObject { // HERE!
let id: UUID = UUID()
// var number: Int
@Published var number: Int // HERE!
init(number: Int) {
self.number = number
}

// mutating func add() {
// self.number += 1
// }

func add() {
self.number += 1
}
}

class ItemManager: ObservableObject {
@Published var items: [Item]
init(items: [Item]) {
self.items = items
}

func add(id: UUID) {
// if let index = items.firstIndex(where: { $0.id == id }) {
// items[index].add()
// }

items.first(where: { $0.id == id })?.add() // Item 为 class 的话,可以直接这样操作。但如果 Item 为 struct 就必须向上面那样操作
self.objectWillChange.send() // HERE!
}

func createItem() {
items.append(Item(number: 3))
}
}

注意,如果在 add() 中不添加 self.objectWillChange.send(),那么第一个 ForEach 中的数字不会变化。

这里实际讨论了两种更新 ObservableObject 的方法,参考: Why is an ObservedObject array not updated in my SwiftUI application?

使用 objectWillChange.send(),对应到第一个 ForEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person: ObservableObject,Identifiable {
var id: Int
@Published var name: String

init(id: Int, name: String){
self.id = id
self.name = name
}
}

class People: ObservableObject {
@Published var people: [Person]

init(){
self.people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")]
}
}

struct ContentView: View {
@ObservedObject var mypeople: People

var body: some View {
VStack{
ForEach(mypeople.people){ person in
Text("\(person.name)")
}
Button(action: {
self.mypeople.people[0].name="Jaime"
self.mypeople.objectWillChange.send() // HERE!
}) {
Text("Add/Change name")
}
}
}
}

Identifiable 类使用 @ObservedObject,对应到第二个 ForEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct PersonRow: View {
@ObservedObject var person: Person // HERE!

var body: some View {
Text(person.name)
}
}

struct ContentView: View {
@ObservedObject var mypeople: People

var body: some View {
VStack{
ForEach(mypeople.people){ person in
PersonRow(person: person)
}
Button(action: {
self.mypeople.people[0].name="Jaime"
}) {
Text("Add/Change name")
}
}
}
}

@StateObject 是对的

当然,最简单的还是使用 @StateObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct Person: Identifiable {
var id: Int
var name: String

init(id: Int, name: String){
self.id = id
self.name = name
}

}

class Model: ObservableObject {
@Published var people: [Person]

init(){
self.people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")]
}

}

struct ContentView: View {
@StateObject var model = Model()

var body: some View {
VStack{
ForEach(model.people){ person in
Text("\(person.name)")
}
Button(action: {
self.mypeople.people[0].name="Jaime"
}) {
Text("Add/Change name")
}
}
}
}

当然,在我们自己的案例中 @StateObject 是不中用的——第二个 ForEach 无论如何是无法刷新的。

@Observable, @Bindable 新的就是好的

参考:

  1. Swift 5.9 新 @Observable 对象在 SwiftUI 使用中的陷阱与解决
  2. SwiftUI5 新增加的Observable宏的基本用法。
  3. SwiftUI 属性包装器系列 — @Observable @Bindable
  4. 深入理解 Observation - 原理,back porting 和性能
  5. 深度解读 Observation —— SwiftUI 性能提升的新途径
  6. 新框架、新思维:解析 Observation 和 SwiftData 框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import SwiftUI
import Observation

struct ContentView: View {

@State var itemManager: ItemManager = ItemManager(items: [
Item(number: 1),
Item(number: 2)
])

var body: some View {

VStack {
ForEach(itemManager.items) { item in
HStack{
Text("\(item.number)")
Button("NUMBER + 1") {
itemManager.add(id: item.id)
}
}
}

Button("Create Item") {
itemManager.createItem()
}

Divider()

ForEach(itemManager.items) { item in
SecView(item: item)
}
}
}
}

struct SecView: View {
@Bindable var item: Item
var body: some View {
Text("\(item.number)")
}
}

@Observable
class Item: Identifiable {
let id: UUID = UUID()
var number: Int
init(number: Int) {
self.number = number
}

func add() {
self.number += 1
}
}

@Observable
class ItemManager {
var items: [Item]
init(items: [Item]) {
self.items = items
}

func add(id: UUID) {
items.first(where: { $0.id == id })?.add()
}

func createItem() {
items.append(Item(number: 3))
}
}

请注意:您永远不会在 @Binding@Bindable 之间进行选择。@Binding 属性包装器表明视图上的某些状态由父视图拥有,并且您对基础数据具有读写访问权限。@Bindable 表示用于创建与符合 Observable 协议的数据模型对象的可变属性的绑定;

另外,使用 SwiftData 时 @Model@Bindable 配合也能实现相同的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Model class Item: Identifiable {
var id: UUID
var value: Int
init(id: UUID, value: Int) {
self.id = id
self.value = value
}
}


struct ContentView: View {
@Query private var items: [Item]
@State var selectItem: Item?
var body: some View {
List {
ForEach(items) { item in
Text("\(item.value)")
.onTapGesture {
selectItem = item
print(item.value)
}
}
}
.sheet(item: $selectItem) { item in
ItemDetailView(item: item)
}
}
}

struct ItemDetailView: View {
@Bindable var item: Item
var body: some View {
VStack() {
Text("\(item.value)")
Button(action: {
item.value = 999
}, label: {
Text("Change Value")
})
}
}
}

SwiftUI @State, @Binding, @StateObject, @ObservedObject, @Observable 等的区别
https://wonderhoi.com/2024/02/18/SwiftUI-StateObject-ObservedObject-Observable-三者的区别/
作者
wonderhoi
发布于
2024年2月18日
许可协议