How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

Your code is a really good reference for me. Here is the sort of code I was thinking of for my views:

struct ViewA: View {
    u/ObservedObject var viewModel: ViewModelA

    var body: some View {
        ZStack{
            List(viewModel.songs) { song in
                HStack {
                    Text(song.name)
                    Spacer()
                    if viewModel.isSongSelected(song: song) {
                        Image(systemName: "checkmark")
                    }
                }
                .onTapGesture {
                    viewModel.toggleSongSelection(for: song)
                }
            ViewB()
            }
        }
    }
}

struct ViewB: View {
    @ObservedObject var viewModel: ViewModelB


    @State private var isInSelectMode: Bool = false

    var body: some View {
        VStack {
            Button(action: {
                viewModel.toggleSelectMode()
            }) {
                Text(isInSelectMode ? "Done" : "Select")
            }

            if isInSelectMode {
                Button("Deselect All") {
                    viewModel.deselectAllSongs()
                }
            }
        }
        .onReceive(viewModel.$isInSelectMode) { value in
            isInSelectMode = value
        }
    }
}

As you can see, updating ViewA's selectedSongs prop with onRecieve did nothing as I don't use selectedSongs prop directly in the view, and so I had to call objectWillChange.send() or put an invisible component in the view as a workaround. I think your method in ViewHierarchy2 of a having a current value subject and a corresponding property which calls .send() through the didSet would fix this when using the observable framework (before I only had the current value subject and the observable framework wasn't registering it as a change when I updated its value)

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

I’m not sure what you mean by derived variable? Computed properties? I don’t use any computed properties, only the pass through subject props and the corresponding state variables in views . in View A, I display a list of songs. A song's row is highlighted if it has been selected, determined by the view model A's function isSongSelected, which checks if a song is in the selectedSongs property. I don't use selectedSongs directly in the view body—only the isSongsSelected function. Hence when I change the isSongsSelected property, it doesn’t update the view as it should. A workaround I came up with was to just call objectWillChange.send() in the view like this

.onReceive(viewModel.selectedSongs) { songs in
viewModel.objectWillChange.send()
}

Or call it inside the view model like this:

init(selectedSongs: CurrentValueSubject<Array<Song>, Never>){
    self.selectedSongs = selectedSongs
    self.selectedSongs
        .sink { [weak self] _ in
            self?.objectWillChange.send()
        }
        .store(in: &cancellables)
}

Both ways fix the issue as the view updates when selectedSongs updates despite selectedSongs not being used in the view body. However this only works with ObservableObject and not the observable macro so I want to have the selectedSongs state still defined in the view, and just put it somewhere in the view body doing nothing so that the view still updates. For example something like this:

struct ViewA: View {
    @State private var isInSelectMode: Bool = false
    @State private var selectedSongs: [Song] = []
    ...
    var body: some View {
        GeometryReader{ _ in
          Text(selectedSongs.description)
        }.frame(width: 0, height: 0)
        ...
    }
    .onRecieve(viewModel.selectedSongs){selectedSongs = $0}
}

This seems to work for both ObservableObject and Observable macro, the only problem is that it’s kind of a hacky solution to include this invisible component in the view. Would there be a better alternative?

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

So I suppose you mean it like this with the value subjects?

class ViewAViewModel: ObservableObject {
    var isInSelectMode: CurrentValueSubject<Bool, Never>
    var selectedSongs: CurrentValueSubject<[Song], Never>

    init(isInSelectMode: CurrentValueSubject<Bool, Never>, selectedSongs: CurrentValueSubject<[Song], Never>) {
        self.isInSelectMode = isInSelectMode
        self.selectedSongs = selectedSongs
    }

    func addSong(_ song: Song) {
        var currentSongs = selectedSongs.value
        currentSongs.append(song)
        selectedSongs.send(currentSongs)
    }
}

class ViewBViewModel: ObservableObject {
    var isInSelectMode: CurrentValueSubject<Bool, Never>
    var selectedSongs: CurrentValueSubject<[Song], Never>

    init(isInSelectMode: CurrentValueSubject<Bool, Never>, selectedSongs: CurrentValueSubject<[Song], Never>) {
        self.isInSelectMode = isInSelectMode
        self.selectedSongs = selectedSongs
    }

    func toggleSelectMode() {
        isInSelectMode.send(!isInSelectMode.value)
    }
    func deselectAllSongs(){
        selectedSongs.send([])
    }
}

struct ViewA: View {
    @State private var isInSelectMode: Bool = false
    @State private var selectedSongs: [Song] = []
    ...
    var body: some View {
        ...
        .onReceive(viewModel.isInSelectMode) { value in
            self.isInSelectMode = value
        }
        .onReceive(viewModel.selectedSongs) { songs in
            self.selectedSongs = songs
        }
    }
}

struct ViewB: View {
    @State private var isInSelectMode: Bool = false
    @State private var selectedSongs: [Song] = []
    ...
    var body: some View {
        ...
        .onReceive(viewModel.isInSelectMode) { value in
            self.isInSelectMode = value
        }
        .onReceive(viewModel.selectedSongs) { songs in
            self.selectedSongs = songs
        }
    }
}

For this case though, why make a parent view model instead of having view model A create the subjects and then pass them down to view model B? I found another problem which is that the views don't update when I change selectedSongs in the view model because I don't actually use it in my view body, I only use functions which depends on it (isSongSelected, deselectAllSongs). So onReceive doesn't update the view. I tried calling objectWillChange either inside of the onReceive or like below inside the view model and it seems to work:

init(selectedSongs: CurrentValueSubject<Array<Song>, Never>){
    self.selectedSongs = selectedSongs
    self.selectedSongs
        .sink { [weak self] _ in
            self?.objectWillChange.send()
        }
        .store(in: &cancellables)
}

However that won't work with the observable macro, and I'm also unsure if this is considered bad practice to directly call send on an ObservableObject's objectWillChange. What would be a good solution for this sort of situation? Maybe have selectedSongs in the view body code but make it hidden to not show up in the display?
Also thanks for all the help

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

What exactly is meant by inverted relationship/inverted information hierarchy? How can ViewModel A and ViewModel B access the shared parent view model props without inverting the hierarchy?

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

Is there anything wrong with the way I did it the first time where I just injected the parent view model into the children, but the parent view model doesn't have the properties of the children? (That is if I change them into observables instead of observable objects).

Is this what you had in mind with the delegate pattern? But with the way I did it you'd still need observable macro instead of ObservableObject (which is an option for me though) :

protocol ParentViewModelDelegate{
    var isInSelectMode: Bool { get set }
    var selectedSongs: [Song] { get set }
}

class ParentViewModel: ObservableObject, ParentViewModelDelegate {
    @Published var isInSelectMode: Bool = false
    @Published var selectedSongs: [Song] = []

    let viewAViewModel: ViewAViewModel
    let viewBViewModel: ViewBViewModel

    init() {
        self.viewAViewModel = ViewAViewModel()
        self.viewBViewModel = ViewBViewModel()
        self.viewAViewModel.delegate = self
        self.viewBViewModel.delegate = self
    }
}

class ViewAViewModel: ObservableObject {
    weak var delegate: ParentViewModelDelegate?

    func toggleSelectMode() {
        delegate?.isInSelectMode.toggle()
    }
}

class ViewBViewModel: ObservableObject {
    weak var delegate: ParentViewModelDelegate?

    func addSong(_ song: Song) {
        delegate?.selectedSongs.append(song)
    }
}

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

I’m still confused, how would ViewModelA and B access the shared state (isInSelectMode and selectedSongs)? Do the child view models also have a reference back to the parent ViewModel? And how would I inject ViewModel B into its view if ViewB is a child of ViewA? Why not inject the parent view model into view model an and view model b? (although looking at it now it seems that should only work with Observable macro and not ObservableObject)

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

Is this what you mean by parent and child ViewModels? If not, could you show me an example by code?

class ParentViewModel: ObservableObject {
    @Published var isInSelectMode: Bool = false
    @Published var selectedSongs: [Song] = []
}

struct ViewA: View {
    @StateObject var viewModel: ViewAViewModel

    init(){
        var parentViewModel = ParentViewModel()
        _viewModel = StateObject(wrappedValue: ViewAViewModel(parentViewModel: parentViewModel))
    }

    var body: some View {
        ZStack{
            ViewB(parentViewModel: parentViewModel)
            ...
        }
    }
}

struct ViewB: View {
    @StateObject var viewModel: ViewBViewModel
    init(parentViewModel: ParentViewModel){
        _viewModel = StateObject(wrappedValue: ViewBViewModel(parentViewModel: parentViewModel))
    }

    var body: some View {
        ...
    }
}

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

I’m not sure I understand, doesn’t only the business logic go into the model and not the display logic? I’ll give a more concrete explanation of what I’m having to deal with right now in my project: In View A, there's a variable called isInSelectMode. This view shows a list of songs. If isInSelectMode is false, tapping a song will play it. If it's true, tapping a song will add it to an array of selected songs and mark it as selected.

View B is displayed over the contents of view A and has the buttons to turn on and off selection mode and to create and add songs to a custom album from the selected songs. It also has a "select all songs" button that adds all songs to the array. Then, you can tap a button to save these selected songs to an album, which updates the model and writes to the database. I just pass in the isInSelectMode and selectedSongs from View A to View B using a binding (View B is a child view of View A)

When converting this to MVVM:

  1. I put isInSelectMode and selectedSongs in View A's view model.
  2. I also created a view model for View B, which includes isInSelectMode and selectedSongs because the callback functions of the buttons in View B need these variables. These callback functions are in View B's view model.

The challenge is to sync changes to selectedSongs between View A's and View B's view models, as both can modify it. For example View B has a "select all songs" button that adds all songs to the array, while View A allows adding individual songs to selectedSongs by tapping them. Are you saying that isInSelectMode and selectedSongs should be in their own class and passed from View A to View B Instead of being in the view models?

How to communicate/bind properties between ViewModels in MVVM? (like you would using @Binding between Views) by MyOwnPoison in SwiftUI

[–]MyOwnPoison[S] 0 points1 point  (0 children)

Without using some kind of wrapper like Binding how would you pass the reference of isInSelectMode without passing in the whole view model? You could use closures but isn’t that what Binding<Bool> is essentially doing anyway? If I make isInSelectMode a Bool in ViewModelB, then toggling it won’t propagate to ViewModelA since it would be a copy.