为什么我的 SwiftUI 应用程序中的 ObservedObject 数组没有更新?
Why is an ObservedObject array not updated in my SwiftUI application?
我正在玩 SwiftUI,试图了解 ObservableObject
的工作原理。我有一个 Person
对象数组。当我将新的 Person
添加到数组中时,它会重新加载到我的视图中,但是如果我更改现有 Person
的值,它不会重新加载到视图中。
// NamesClass.swift
import Foundation
import SwiftUI
import Combine
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.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
如果我取消注释行以添加新的 Person
(John),Jaime 的名字会正确显示,但是如果我只是更改名字,它不会显示在视图中。
恐怕我做错了什么,或者我不明白 ObservedObjects
如何处理数组。
您可以使用结构代替 class。由于结构的值语义,对人名的更改被视为对 Person 结构本身的更改,并且此更改也是对 people 数组的更改,因此 @Published 将发送通知并重新计算视图主体。
import Foundation
import SwiftUI
import Combine
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")
}
}
}
}
或者(不推荐),Person
是一个 class,所以它是一个引用类型。当它发生变化时,People
数组保持不变,因此主体不会发出任何内容。但是,您可以手动调用它,让它知道:
Button(action: {
self.mypeople.objectWillChange.send()
self.mypeople.people[0].name="Jaime"
}) {
Text("Add/Change name")
}
对于那些可能会觉得有用的人。这是对@kontiki 的回答的一种更通用的方法。
这样您就不必为不同的模型 class 类型重复自己
import Foundation
import Combine
import SwiftUI
class ObservableArray<T>: ObservableObject {
@Published var array:[T] = []
var cancellables = [AnyCancellable]()
init(array: [T]) {
self.array = array
}
func observeChildrenChanges<T: ObservableObject>() -> ObservableArray<T> {
let array2 = array as! [T]
array2.forEach({
let c = [=10=].objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
return self as! ObservableArray<T>
}
}
class Person: ObservableObject,Identifiable{
var id: Int
@Published var name: String
init(id: Int, name: String){
self.id = id
self.name = name
}
}
struct ContentView : View {
//For observing changes to the array only.
//No need for model class(in this case Person) to conform to ObservabeObject protocol
@ObservedObject var mypeople: ObservableArray<Person> = ObservableArray(array: [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")])
//For observing changes to the array and changes inside its children
//Note: The model class(in this case Person) must conform to ObservableObject protocol
@ObservedObject var mypeople: ObservableArray<Person> = try! ObservableArray(array: [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")]).observeChildrenChanges()
var body: some View {
VStack{
ForEach(mypeople.array){ person in
Text("\(person.name)")
}
Button(action: {
self.mypeople.array[0].name="Jaime"
//self.mypeople.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
我认为这个问题有更优雅的解决方案。您可以为列表行创建一个自定义视图,而不是尝试将 objectWillChange
消息传播到模型层次结构中,这样每个项目都是一个 @ObservedObject:
struct PersonRow: View {
@ObservedObject var person: Person
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"
//self.mypeople.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
通常,为 List/ForEach 中的项目创建自定义视图允许监视集合中的每个项目的更改。
ObservableArray 很有用,谢谢!这是一个支持所有集合的更通用的版本,当您需要对通过 to-many 关系(建模为集合)间接的 CoreData 值做出反应时,它会很方便。
import Combine
import SwiftUI
private class ObservedObjectCollectionBox<Element>: ObservableObject where Element: ObservableObject {
private var subscription: AnyCancellable?
init(_ wrappedValue: AnyCollection<Element>) {
self.reset(wrappedValue)
}
func reset(_ newValue: AnyCollection<Element>) {
self.subscription = Publishers.MergeMany(newValue.map{ [=10=].objectWillChange })
.eraseToAnyPublisher()
.sink { _ in
self.objectWillChange.send()
}
}
}
@propertyWrapper
public struct ObservedObjectCollection<Element>: DynamicProperty where Element: ObservableObject {
public var wrappedValue: AnyCollection<Element> {
didSet {
if isKnownUniquelyReferenced(&observed) {
self.observed.reset(wrappedValue)
} else {
self.observed = ObservedObjectCollectionBox(wrappedValue)
}
}
}
@ObservedObject private var observed: ObservedObjectCollectionBox<Element>
public init(wrappedValue: AnyCollection<Element>) {
self.wrappedValue = wrappedValue
self.observed = ObservedObjectCollectionBox(wrappedValue)
}
public init(wrappedValue: AnyCollection<Element>?) {
self.init(wrappedValue: wrappedValue ?? AnyCollection([]))
}
public init<C: Collection>(wrappedValue: C) where C.Element == Element {
self.init(wrappedValue: AnyCollection(wrappedValue))
}
public init<C: Collection>(wrappedValue: C?) where C.Element == Element {
if let wrappedValue = wrappedValue {
self.init(wrappedValue: wrappedValue)
} else {
self.init(wrappedValue: AnyCollection([]))
}
}
}
它可以按如下方式使用,例如,我们有一个 class 冰箱,其中包含一个 Set,尽管没有任何子视图观察每个项目,但我们的视图需要对后者的变化做出反应。
class Food: ObservableObject, Hashable {
@Published var name: String
@Published var calories: Float
init(name: String, calories: Float) {
self.name = name
self.calories = calories
}
static func ==(lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name && lhs.calories == rhs.calories
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.name)
hasher.combine(self.calories)
}
}
class Fridge: ObservableObject {
@Published var food: Set<Food>
init(food: Set<Food>) {
self.food = food
}
}
struct FridgeCaloriesView: View {
@ObservedObjectCollection var food: AnyCollection<Food>
init(fridge: Fridge) {
self._food = ObservedObjectCollection(wrappedValue: fridge.food)
}
var totalCalories: Float {
self.food.map { [=11=].calories }.reduce(0, +)
}
var body: some View {
Text("Total calories in fridge: \(totalCalories)")
}
}
理想的做法是链接 @ObservedObject
或 @StateObject
和其他一些适合序列的 属性 包装器,例如@StateObject @ObservableObjects
。但是你不能使用超过一个 属性 包装器,所以你需要制作不同的类型来处理两种不同的情况。然后,您可以根据需要使用以下任一方法。
(您的 People
类型是不必要的——它的目的可以抽象到所有序列。)
@StateObjects var people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")
]
@ObservedObjects var people: [Person]
import Combine
import SwiftUI
@propertyWrapper
public final class ObservableObjects<Objects: Sequence>: ObservableObject
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
self.wrappedValue = wrappedValue
assignCancellable()
}
@Published public var wrappedValue: Objects {
didSet { assignCancellable() }
}
private var cancellable: AnyCancellable!
}
// MARK: - private
private extension ObservableObjects {
func assignCancellable() {
cancellable = Publishers.MergeMany(wrappedValue.map(\.objectWillChange))
.sink { [unowned self] _ in objectWillChange.send() }
}
}
// MARK: -
@propertyWrapper
public struct ObservedObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
@ObservedObject private var objects: ObservableObjects<Objects>
}
@propertyWrapper
public struct StateObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
@StateObject private var objects: ObservableObjects<Objects>
}
我正在玩 SwiftUI,试图了解 ObservableObject
的工作原理。我有一个 Person
对象数组。当我将新的 Person
添加到数组中时,它会重新加载到我的视图中,但是如果我更改现有 Person
的值,它不会重新加载到视图中。
// NamesClass.swift
import Foundation
import SwiftUI
import Combine
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.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
如果我取消注释行以添加新的 Person
(John),Jaime 的名字会正确显示,但是如果我只是更改名字,它不会显示在视图中。
恐怕我做错了什么,或者我不明白 ObservedObjects
如何处理数组。
您可以使用结构代替 class。由于结构的值语义,对人名的更改被视为对 Person 结构本身的更改,并且此更改也是对 people 数组的更改,因此 @Published 将发送通知并重新计算视图主体。
import Foundation
import SwiftUI
import Combine
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")
}
}
}
}
或者(不推荐),Person
是一个 class,所以它是一个引用类型。当它发生变化时,People
数组保持不变,因此主体不会发出任何内容。但是,您可以手动调用它,让它知道:
Button(action: {
self.mypeople.objectWillChange.send()
self.mypeople.people[0].name="Jaime"
}) {
Text("Add/Change name")
}
对于那些可能会觉得有用的人。这是对@kontiki 的回答的一种更通用的方法。
这样您就不必为不同的模型 class 类型重复自己
import Foundation
import Combine
import SwiftUI
class ObservableArray<T>: ObservableObject {
@Published var array:[T] = []
var cancellables = [AnyCancellable]()
init(array: [T]) {
self.array = array
}
func observeChildrenChanges<T: ObservableObject>() -> ObservableArray<T> {
let array2 = array as! [T]
array2.forEach({
let c = [=10=].objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
return self as! ObservableArray<T>
}
}
class Person: ObservableObject,Identifiable{
var id: Int
@Published var name: String
init(id: Int, name: String){
self.id = id
self.name = name
}
}
struct ContentView : View {
//For observing changes to the array only.
//No need for model class(in this case Person) to conform to ObservabeObject protocol
@ObservedObject var mypeople: ObservableArray<Person> = ObservableArray(array: [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")])
//For observing changes to the array and changes inside its children
//Note: The model class(in this case Person) must conform to ObservableObject protocol
@ObservedObject var mypeople: ObservableArray<Person> = try! ObservableArray(array: [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")]).observeChildrenChanges()
var body: some View {
VStack{
ForEach(mypeople.array){ person in
Text("\(person.name)")
}
Button(action: {
self.mypeople.array[0].name="Jaime"
//self.mypeople.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
我认为这个问题有更优雅的解决方案。您可以为列表行创建一个自定义视图,而不是尝试将 objectWillChange
消息传播到模型层次结构中,这样每个项目都是一个 @ObservedObject:
struct PersonRow: View {
@ObservedObject var person: Person
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"
//self.mypeople.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
通常,为 List/ForEach 中的项目创建自定义视图允许监视集合中的每个项目的更改。
ObservableArray 很有用,谢谢!这是一个支持所有集合的更通用的版本,当您需要对通过 to-many 关系(建模为集合)间接的 CoreData 值做出反应时,它会很方便。
import Combine
import SwiftUI
private class ObservedObjectCollectionBox<Element>: ObservableObject where Element: ObservableObject {
private var subscription: AnyCancellable?
init(_ wrappedValue: AnyCollection<Element>) {
self.reset(wrappedValue)
}
func reset(_ newValue: AnyCollection<Element>) {
self.subscription = Publishers.MergeMany(newValue.map{ [=10=].objectWillChange })
.eraseToAnyPublisher()
.sink { _ in
self.objectWillChange.send()
}
}
}
@propertyWrapper
public struct ObservedObjectCollection<Element>: DynamicProperty where Element: ObservableObject {
public var wrappedValue: AnyCollection<Element> {
didSet {
if isKnownUniquelyReferenced(&observed) {
self.observed.reset(wrappedValue)
} else {
self.observed = ObservedObjectCollectionBox(wrappedValue)
}
}
}
@ObservedObject private var observed: ObservedObjectCollectionBox<Element>
public init(wrappedValue: AnyCollection<Element>) {
self.wrappedValue = wrappedValue
self.observed = ObservedObjectCollectionBox(wrappedValue)
}
public init(wrappedValue: AnyCollection<Element>?) {
self.init(wrappedValue: wrappedValue ?? AnyCollection([]))
}
public init<C: Collection>(wrappedValue: C) where C.Element == Element {
self.init(wrappedValue: AnyCollection(wrappedValue))
}
public init<C: Collection>(wrappedValue: C?) where C.Element == Element {
if let wrappedValue = wrappedValue {
self.init(wrappedValue: wrappedValue)
} else {
self.init(wrappedValue: AnyCollection([]))
}
}
}
它可以按如下方式使用,例如,我们有一个 class 冰箱,其中包含一个 Set,尽管没有任何子视图观察每个项目,但我们的视图需要对后者的变化做出反应。
class Food: ObservableObject, Hashable {
@Published var name: String
@Published var calories: Float
init(name: String, calories: Float) {
self.name = name
self.calories = calories
}
static func ==(lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name && lhs.calories == rhs.calories
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.name)
hasher.combine(self.calories)
}
}
class Fridge: ObservableObject {
@Published var food: Set<Food>
init(food: Set<Food>) {
self.food = food
}
}
struct FridgeCaloriesView: View {
@ObservedObjectCollection var food: AnyCollection<Food>
init(fridge: Fridge) {
self._food = ObservedObjectCollection(wrappedValue: fridge.food)
}
var totalCalories: Float {
self.food.map { [=11=].calories }.reduce(0, +)
}
var body: some View {
Text("Total calories in fridge: \(totalCalories)")
}
}
理想的做法是链接 @ObservedObject
或 @StateObject
和其他一些适合序列的 属性 包装器,例如@StateObject @ObservableObjects
。但是你不能使用超过一个 属性 包装器,所以你需要制作不同的类型来处理两种不同的情况。然后,您可以根据需要使用以下任一方法。
(您的 People
类型是不必要的——它的目的可以抽象到所有序列。)
@StateObjects var people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")
]
@ObservedObjects var people: [Person]
import Combine
import SwiftUI
@propertyWrapper
public final class ObservableObjects<Objects: Sequence>: ObservableObject
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
self.wrappedValue = wrappedValue
assignCancellable()
}
@Published public var wrappedValue: Objects {
didSet { assignCancellable() }
}
private var cancellable: AnyCancellable!
}
// MARK: - private
private extension ObservableObjects {
func assignCancellable() {
cancellable = Publishers.MergeMany(wrappedValue.map(\.objectWillChange))
.sink { [unowned self] _ in objectWillChange.send() }
}
}
// MARK: -
@propertyWrapper
public struct ObservedObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
@ObservedObject private var objects: ObservableObjects<Objects>
}
@propertyWrapper
public struct StateObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
@StateObject private var objects: ObservableObjects<Objects>
}