Published works for single object but not for arra

2020-06-21 10:34发布

问题:

I am trying to make individually moveable objects. I am able to successfully do it for one object but once I place it into an array, the objects are not able to move anymore.

Model:

class SocialStore: ObservableObject {
    @Published var socials : [Social]

    init(socials: [Social]){
        self.socials = socials
    }
}

class Social : ObservableObject{
    var id: Int
    var imageName: String
    var companyName: String
    @Published var pos: CGPoint

    init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
        self.id = id
        self.imageName = imageName
        self.companyName = companyName
        self.pos = pos
    }

    var dragGesture : some Gesture {
        DragGesture()
            .onChanged { value in
                self.pos = value.location
                print(self.pos)
        }
    }
}

Multiple image (images not following drag):

struct ContentView : View {
    @ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)

    @ObservedObject var images: Social = testData[2]

    var body: some View {
        VStack {
            ForEach(socialObject.socials, id: \.id) { social in
                Image(social.imageName)
                    .position(social.pos)
                    .gesture(social.dragGesture)
            }
        }
    }
}

Single image (image follow gesture):

struct ContentView : View {
    @ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)

    @ObservedObject var images: Social = testData[2]

    var body: some View {
        VStack {
            Image(images.imageName)
                .position(images.pos)
                .gesture(images.dragGesture)
        }
    }
}

I expect the individual items to be able to move freely . I see that the coordinates are updating but the position of each image is not.

回答1:

First, a disclaimer: The code below is not meant as a copy-and-paste solution. Its only goal is to help you understand the challenge. There may be more efficient ways of resolving it, so take your time to think of your implementation once you understand the problem.


Why the view does not update?: The @Publisher in SocialStore will only emit an update when the array changes. Since nothing is being added or removed from the array, nothing will happen. Additionally, because the array elements are objects (and not values), when they do change their position, the array remains unaltered, because the reference to the objects remains the same. Remember: Classes create objects, Structs create values.

We need a way of making the store, to emit a change when something in its element changes. In the example below, your store will subscribe to each of its elements bindings. Now, all published updates from your items, will be relayed to your store publisher, and you will obtain the desired result.

import SwiftUI
import Combine

class SocialStore: ObservableObject {
    @Published var socials : [Social]
    var cancellables = [AnyCancellable]()

    init(socials: [Social]){
        self.socials = socials

        self.socials.forEach({
            let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() })

            // Important: You have to keep the returned value allocated,
            // otherwise the sink subscription gets cancelled
            self.cancellables.append(c)
        })
    }
}

class Social : ObservableObject{
    var id: Int
    var imageName: String
    var companyName: String

    @Published var pos: CGPoint

    init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
        self.id = id
        self.imageName = imageName
        self.companyName = companyName
        self.pos = pos
    }

    var dragGesture : some Gesture {
        DragGesture()
            .onChanged { value in
                self.pos = value.location
                print(self.pos)
        }
    }
}

struct ContentView : View {
    @ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)

    var body: some View {
        VStack {
            ForEach(socialObject.socials, id: \.id) { social in
                Image(social.imageName)
                    .position(social.pos)
                    .gesture(social.dragGesture)
            }
        }
    }
}


回答2:

There are two ObservableObject types and the one that you are interested in is Combine.ObservableObject. It requires an objectWillChange variable of type ObservableObjectPublisher and it is this that SwiftUI uses to trigger a new rendering. I am not sure what Foundation.ObservableObject is used for but it is confusing.

@Published creates a PassthroughSubject publisher that can be connected to a sink somewhere else but which isn't useful to SwiftUI, except for .onReceive() of course.



回答3:

You need to implement

let objectWillChange = ObservableObjectPublisher()

in your ObservableObject class



回答4:

For those who might find it helpful. This is a more generic approach to @kontiki 's answer.

This way you will not have to be repeating yourself for different model class types.

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<K>(_ type:K.Type) throws ->ObservableArray<T> where K : ObservableObject{
        let array2 = array as! [K]
        array2.forEach({
            let c = $0.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
    }

}

class Social : ObservableObject{
    var id: Int
    var imageName: String
    var companyName: String
    @Published var pos: CGPoint

    init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
        self.id = id
        self.imageName = imageName
        self.companyName = companyName
        self.pos = pos
    }

    var dragGesture : some Gesture {
        DragGesture()
            .onChanged { value in
                self.pos = value.location
                print(self.pos)
        }
    }
}

struct ContentView : View {
    //For observing changes to the array only. 
    //No need for model class(in this case Social) to conform to ObservabeObject protocol
    @ObservedObject var socialObject: ObservableArray<Social> = ObservableArray(array: testData)

    //For observing changes to the array and changes inside its children
    //Note: The model class(in this case Social) must conform to ObservableObject protocol
    @ObservedObject var socialObject: ObservableArray<Social> = try! ObservableArray(array: testData).observeChildrenChanges(Social.self)

    var body: some View {
        VStack {
            ForEach(socialObject.array, id: \.id) { social in
                Image(social.imageName)
                    .position(social.pos)
                    .gesture(social.dragGesture)
            }
        }
    }
}