I just stuck trying to properly implement MVVM pattern in SwiftUI
I got such application
ContainerView is the most common view. It contains single business View Model object - ItemsViewModel and one surrogate - Bool to toggle it's value and force re-render of whole View. ItemsView contains of array of business objects - ItemView
What I want is to figure out:
- how to implement bindings which actually works❓
- call back event and pass value from child view to parent❓
I came from React-Redux world, it done easy there. But for the several days I cant figure out what should I do... I chose MVVM though as I also got some WPF experience and thought it'll give some boost, there its done with ObservableCollection for bindable arrays
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
@Published var items: ItemsViewModel;
// variable used to refresh most common view
@Published var refresh: Bool = false;
init() {
self.items = ItemsViewModel();
}
func buttonRefresh_onClick() -> Void {
self.refresh.toggle();
}
func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemViewModel())
}
}
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
@StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: $viewModel.items).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
Button(action: viewModel.buttonRefresh_onClick) {
Text("Refresh")
}.padding()
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
@Published var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemsView.swift⤵️
struct ItemsView: View {
@Binding var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.count)")
ScrollView(.vertical) {
ForEach($viewModel.items) { item in
ItemView(viewModel: item).padding()
}
}
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item form ItemsView")
}
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
@Published public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
@Published public var intProp: Int;
init() {
self.intProp = 0;
}
func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
//todo ❗ I want to delete item in parent component
}
}
ItemView.swift⤵️
struct ItemView: View {
@Binding var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.intProp)")
Button(action: viewModel.buttonIncrementIntProp_onClick) {
Image(systemName: "plus")
}
Button(action: viewModel.buttonDelete_onClick) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
I read official docs and countless SO topics and articles, but nowhere got solution for exact my case (or me doing something wrong). It only works if implement all UI part in single view
UPD 1:
Is it even possible to with class but not a struct in View Model?
Updates works perfectly if I use struct
instead of class
:
ItemsViewModel.swift⤵️
struct ItemsViewModel {
var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
mutating func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemViewModel.swift⤵️
struct ItemViewModel: Identifiable, Equatable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
mutating func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
}
}
But is it ok to use mutating
functions? I also tried to play with Combine
and objectWillChange
, but unable to make it work
UPD 2
Thanks @Yrb for response. With your suggestion and this article I came added Model
struct
ures and ended up with such results:
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
@StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: ItemsViewModel(viewModel.items)).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
}
}
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
@Published var items: ItemsModel;
init() {
self.items = ItemsModel();
}
@MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel())
}
}
ContainerModel.swift⤵️
struct ContainerModel {
public var items: ItemsModel;
}
ItemsView.swift⤵️
struct ItemsView: View {
@ObservedObject var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.items.count)")
ScrollView(.vertical) {
ForEach(viewModel.items.items) { item in
ItemView(viewModel: ItemViewModel(item)).padding()
}
}
Button(action: {
viewModel.buttonAddItem_onClick()
}) {
Text("Add item form ItemsView")
}
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
@Published var items: ItemsModel;
init(_ items: ItemsModel) {
self.items = items;
}
@MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel());
}
}
ItemsModel.swift⤵️
struct ItemsModel {
public var items: [ItemModel] = [ItemModel]();
}
ItemView.swift⤵️
struct ItemView: View {
@StateObject var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.item.intProp)")
Button(action: {
viewModel.buttonIncrementIntProp_onClick()
}) {
Image(systemName: "plus")
}
Button(action: {
viewModel.buttonDelete_onClick()
}) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
@Published private(set) var item: ItemModel;
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
init(_ item: ItemModel) {
self.item = item;
self.item.intProp = 0;
}
@MainActor func buttonIncrementIntProp_onClick() -> Void {
self.item.intProp = self.item.intProp + 1;
}
@MainActor func buttonDelete_onClick() -> Void {
}
}
ItemModel.swift⤵️
struct ItemModel: Identifiable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
}
This code runs and works perfectly, at least I see no problems. But I'm not shure if I properly initializes and "bind" ViewModels and Models - code looks quite messy. Also I'n not shure I correctly set ObservedObject
in ItemsView
and StateObject
in ItemView
. So please check me