William Hu
William Hu

Reputation: 16141

SwiftUI How to assign @Published to another @Published

I have a view(MainView) with another view(FooGroupView) inside. Each time I click the item in FooGroupView then pass the value changes to MainView.

Like below:

The white view is MainView, the red is FooGroupView. If click foo1 then the text should Current:foo1, then if click foo2 then Current:foo2.

enter image description here

My code is here:

Test.Playground

import SwiftUI
import PlaygroundSupport

struct FooRowView: View {

    var foo: Foo

    var body: some View {
        VStack {
            Color(.red)
            Text(foo.name)
        }
    }
}

class Foo: Identifiable, ObservableObject {
    var name: String
    var id: String

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

final class FooGroupViewModel: ObservableObject {

    var foos: [Foo] {
        didSet {
            self.selectedFoo = foos.first
        }
    }

    @Published var selectedFoo: Foo? {
        didSet {
            print("You selected: \(selectedFoo?.name)")
        }
    }

    init(foos: [Foo] = []) {
        self.foos = foos
        self.selectedFoo = foos.first
    }
}

struct FooGroupView: View {

    var viewModel: FooGroupViewModel

    var body: some View {
        ScrollView(.horizontal) {
            HStack(alignment: .center, spacing: 32, content: {

                // Error: error: Execution was interrupted, reason: signal SIGABRT.
                //The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

                /*ForEach(viewModel.foos) { foo in
                    Text(foo.name)
                }*/


                Text(viewModel.foos[0].name).onTapGesture {
                    viewModel.selectedFoo = viewModel.foos[0]
                }
                Text(viewModel.foos[1].name).onTapGesture {
                    viewModel.selectedFoo = viewModel.foos[1]
                }
            })
        }
    }
}

final class MainViewModel: ObservableObject {
    @ObservedObject var fooGroupViewModel: FooGroupViewModel

    @Published var currentFoo: Foo?

    init() {
        fooGroupViewModel = FooGroupViewModel(foos: [Foo(name: "foo1", id: "1"), Foo(name: "foo2", id: "2")])
        currentFoo = self.fooGroupViewModel.selectedFoo

    }
}

struct MainView: View {

    @ObservedObject var viewModel = MainViewModel()

    var body: some View {
        VStack {
            FooGroupView(viewModel: viewModel.fooGroupViewModel)
                .background(Color.red)
            Spacer()
            Text("Current:\(viewModel.currentFoo!.name)")
        }.frame(width: 200, height: 200)

    }
}


PlaygroundPage.current.liveView = UIHostingController(rootView: MainView())

In MainViewModel if currentFoo has changes, then the UI should be updated.

When click foo1 or foo2, the selectedFoo in FooGroupViewModel was updated, then in MainViewModel should get the changes (as @ObservedObject var fooGroupViewModel: FooGroupViewModel)

My question is how to let currentFoo knows about the changes of selectedFoo in fooGroupViewModel? I was thought this line could observe the changes of the selectedFoo and if any update then trigger the currentFoo changes and update the UI.

currentFoo = self.fooGroupViewModel.selectedFoo

But actually it doesn't work.

Any help? thanks!

I also have another question, its the comment in above code(Line 56 if paste my code into Playground)

ForEach(viewModel.foos) { foo in
                    Text(foo.name)
                }

this code make the playground error:

// Error: error: Execution was interrupted, reason: signal SIGABRT.
                //The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

without any other information in console. Not sure why. Thanks for any help.

Upvotes: 1

Views: 3493

Answers (1)

New Dev
New Dev

Reputation: 49580

Just doing currentFoo = self.fooGroupViewModel.selectedFoo definitely wouldn't work - it just assigns to currentFoo whatever selectedFoo is at the time.

With your setup, you would need to subscribe to changes in fooGroupViewModel.selectedFoo, which could be done via a Published publisher, available because it's a @Published property.

Also, bear in mind that @ObservedObject only makes sense inside a View, so I removed it.

import Combine

// ...
 
final class MainViewModel: ObservableObject {

    var fooGroupViewModel: FooGroupViewModel
    @Published var currentFoo: Foo?

    private var cancellables: Set<AnyCancellable> = []

    init() {
        fooGroupViewModel = FooGroupViewModel(foos: 
                     [Foo(name: "foo1", id: "1"), Foo(name: "foo2", id: "2")])
        
        fooGroupViewModel.$selectedFoo
           .assign(to: \.currentFoo, on: self)
           .store(in: &cancellables)

    }
}

Upvotes: 9

Related Questions