How to pass data between ViewModels in SwiftUI - ios

I have this use case where I have a parent view and a child view. Both of the views have their own corresponding ViewModels.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
ParentViewModel:
class ParentViewModel: ObservableObject {
#Published var newListName: String = ""
func saveList() {
// some logic to save to CoreData, method would be called via a button
// how do I reference "someString" from ChildViewModel in this ViewModel?
}
}
ChildView:
struct ChildView: View {
#StateObject var childViewModel = ChildViewModel()
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
childViewModel.alterData()
}
}
}
}
}
ChildViewModel:
class ChildViewModel: ObservableObject {
#Published var someString: String = ""
func alterData() {
someString = "Toast"
}
}
My question now is, how do I pass the new value of "someString" from ChildViewModel into the ParentViewModel, in order to do some further stuff with it?
I've tried to create a #StateObject var childViewModel = ChildViewModel() reference in the ParentViewModel, but that does obviously not work, as this will create a new instance of the ChildViewModel and therefore not know of the changes made to "someString"
Solution:
As proposed by Josh, I went with the approach to use a single ViewModel instead of two. To achieve this, the ParentView needs a .environmentObject(T) modifier.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}.environmentObject(parentViewModel)
}
The ChildView then references that environment Object via #EnvironmentObject without an initializer:
struct ChildView: View {
#EnvironmentObject var parentViewModel: ParentViewModel
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
parentViewModel.alterData()
}
}
}
}
}

Most likely you would use a binding for this situation:
struct ChildView: View {
#Binding var name: String
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
name = "Altered!"
}
}
}
}
}
And in the parent:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView(name: $parentViewModel.newListName)) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
Also, I think you can remove the NavigationView view from ChildView. Having it ParentView is enough.

Related

NavigationStack pushing new View issue with #Published

Weird issue using new NavigationStack. When trying to push the DrinkView for the second time, it's pushed twice and the OrderFood gets view removed from the navigation.
The reason is #Published var openDrinks in the View Model. Is there is any way to solve this issue.
Thanks.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
NavigationLink("Hello", value: "Amr")
// Text("Hello, world!")
}
.navigationTitle("Main")
.padding()
.navigationDestination(for: String.self) { value in
OrderFood(viewModel: ViewModel())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ViewModel: ObservableObject {
#Published var openDrinks: Bool = false
}
struct OrderFood: View {
#ObservedObject var viewModel: ViewModel
// #ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Add Drink")
.onTapGesture {
viewModel.openDrinks = true
}
}
.navigationTitle("Order Food")
.navigationDestination(isPresented: $viewModel.openDrinks) {
DrinksView()
.navigationTitle("Drinks")
}
.onAppear {
viewModel.openDrinks = false
}
}
}
struct OrderFood_Previews: PreviewProvider {
static var previews: some View {
OrderFood(viewModel: ViewModel())
}
}
import SwiftUI
struct DrinksView: View {
var body: some View {
NavigationLink("Ch") {
Text("Hello, World!")
}
}
}
struct DrinksView_Previews: PreviewProvider {
static var previews: some View {
DrinksView()
}
}
In ContentView, you use OrderFood(viewModel: ViewModel()), which means
you create a new ViewModel every time you navigate to OrderFood.
Try this approach, where you have one source of truth, that you pass to the details view:
struct ContentView: View {
#StateObject var viewModel = ViewModel() // <-- here
var body: some View {
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
NavigationLink("Hello", value: "Amr")
}
.navigationTitle("Main")
.padding()
.navigationDestination(for: String.self) { value in
OrderFood(viewModel: viewModel) // <-- here
}
}
}
}
Note, you could also use #EnvironmentObject in this case.
EDIT-1: using #EnvironmentObject to pass the viewModel around:
struct ContentView: View {
#StateObject var viewModel = ViewModel() // <-- here
var body: some View {
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
NavigationLink("Hello", value: "Amr")
}
.navigationTitle("Main")
.padding()
.navigationDestination(for: String.self) { value in
OrderFood() // <-- here
}
}.environmentObject(viewModel) // <-- here
}
}
struct OrderFood: View {
#EnvironmentObject var viewModel: ViewModel // <-- here
var body: some View {
VStack {
Text("Add Drink")
.onTapGesture {
viewModel.openDrinks = true
}
}
.navigationTitle("Order Food")
.navigationDestination(isPresented: $viewModel.openDrinks) {
DrinksView().navigationTitle("Drinks")
}
.onAppear {
viewModel.openDrinks = false
}
}
}

SwiftUI is it possible to pass range of binding in ForEach?

I'd like to pass a range of an array in a model inside ForEach.
I recreated an example:
import SwiftUI
class TheModel: ObservableObject {
#Published var list: [Int] = [1,2,3,4,5,6,7,8,9,10]
}
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { item in
SubView(subList: $model.list[0..<5]) <-- error if I put a range
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var body: some View {
HStack {
ForEach (subList, id:\.self) { item in
Text("\(item)")
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
The work around
I found is to pass all the list and perform the range inside the subView. But I'd like don't do this because the array is very big:
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { i in
SubView(subList: $model.list, number: i, dimension: 5)
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var number: Int
var dimension: Int
var body: some View {
HStack {
ForEach (subList[number*dimension..<dimension*(number+1)].indices, id:\.self) { idx in
Button(action: {
subList[idx] += 1
print(subList)
}, label: {
Text("num: \(subList[idx])")
})
}
}
}
}
I would pass the model to the subview since it is a class and will be passed by reference and then pass the range as a separate parameter.
Here is my new implementation of SubView
struct SubView: View {
var model: TheModel
var range: Range<Int>
var body: some View {
HStack {
ForEach (model.list[range].indices, id:\.self) { idx in
HStack {
Button(action: {
model.list[idx] += 1
print(model.list)
}, label: {
Text("num: \(model.list[idx])")
})
}
}
}
}
}
Note that I added indices to the ForEach header to make sure we access the array using an index and not with a value from the array.
The calling view would then look like
var body: some View {
VStack {
SubView(model: model, range: (0..<5))
Text("\(model.list.map(String.init).joined(separator: "-"))")
}
The extra Text is just there for testing purposes

Swift - Update List from different View

I have 2 Views in my Swift Project and when I click on the Button on the secondView, I want to update the List in the First View. I don't know how to do it! If I use a static variable in my MainView and then edit this variable from the secondView, it works, but it won't update. And if I don't use static and instead use #State, it would update, but I can't access it from my secondView.
Here is the Code below:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
MainView()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
var arrayList: [CreateListItems] = []
init() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(arrayList) { x in
Text("\(x.name)")
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
func updateList() {
//Code that should remove "FirstName" from the List in MainView
}
var body: some View {
return VStack {
Button(action: {
updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}
struct CreateListItems: Identifiable {
var id: UUID = UUID()
var name: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can share it using #State and #Binding if you put
struct ContentView: View {
#State var arrayList: [CreateListItems] = []
struct MainView: View {
#Binding var arrayList: [CreateListItems]
struct UpdateOtherViewFromHere: View {
#Binding var arrayList: [CreateListItems]
or you use the MVVM pattern and store the list in an ObservableObject and use #StateObject/#ObservedObject (source) and use #EnvironmentObject(connection) to share it between your Views.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
class ParentViewModel: ObservableObject{
#Published var arrayList: [CreateListItems] = []
init(){
addSamples()
}
func addSamples() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
func updateList() {
let a = CreateListItems(name: "\(arrayList.count + 1) Name!")
arrayList.append(a)
}
}
struct ParentView: View {
#StateObject var vm: ParentViewModel = ParentViewModel()
var body: some View {
TabView {
MainView().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(vm.arrayList) { x in
Text(x.name)
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
Button(action: {
vm.updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}

Why the first item of the list is displayed all the on the opened sheet

I am passing binding variable into other view:
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var isSheetIsVisible = false
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.isSheetIsVisible.toggle()
}
.sheet(isPresented: self.$isSheetIsVisible){
PocketDetailsView(pocketItem: self.$pocket.pockets[index])
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
}
}
}
the other view is:
struct PocketDetailsView: View {
#Binding var pocketItem: PocketItem
var body: some View {
Text("\(pocketItem.name)")
}
}
Why I see the first item when i open sheet for second or third row?
When I use NavigationLink instead of the .sheet it works perfect
You activate all sheets at once, try the following approach (I cannot test your code, but the idea should be clear)
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var selectedItem: PocketItem? = nil
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.selectedItem = pocketItem
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
.sheet(item: self.$selectedPocket) { item in
PocketDetailsView(pocketItem:
self.$pocket.pockets[self.pocket.pockets.firstIndex(of: item)!])
}
}
}
}

How to bind objects using MVVM design pattern?

I'm trying to change below code(1.) to MVVM architecture(2.) but can't figure out how to bind objects.
I think the problem is FirstView only pass value but not actually a Binding Object, I tried some different ways but always stuck on don't know how to assign Binding Object to #Published.
Could somebody please give a few hints?
(Works fine)
struct FirstView: View {
#State var showSecondView = false
var body: some View {
Button(action: {
self.showSecondView.toggle()
}) {
Text("Show second view")
}
.sheet(isPresented: $showSecondView) {
SecondView(showSecondView: self.$showSecondView)
}
}
}
struct SecondView: View {
#Binding var showSecondView: Bool
var body: some View {
Button(action: {
self.showSecondView.toggle()
}) {
Text("Dismiss")
}
}
}
(MVVM)
struct FirstView: View {
#ObservedObject var vm = FirstViewModel()
var body: some View {
Button(action: {
self.vm.showSecondView.toggle()
}) {
Text("Show second view")
}
.sheet(isPresented: $vm.showSecondView) {
SecondView2(vm: SecondViewModel(showSecondView: self.vm.showSecondView))
}
}
}
class FirstViewModel: ObservableObject {
#Published var showSecondView = false
}
struct SecondView: View {
#ObservedObject var vm: SecondViewModel
var body: some View {
Button(action: {
self.vm.showSecondView.toggle()
}) {
Text("Dismiss")
}
}
}
class SecondViewModel: ObservableObject {
#Published var showSecondView: Bool
//Right here, i'm not sure how to bind 'showSecondView' from FirstView
init(showSecondView: Bool) {
self.showSecondView = showSecondView
}
}
You can pass directly the Binding<Bool> to the second VM but in this way, there's no need to have #Published var and it also doesn't need to be ObservableObject and marked as #ObservedObject. There might be a better solution to this.
struct FirstView: View {
#ObservedObject var vm = FirstViewModel()
var body: some View {
Button(action: {
self.vm.showSecondView.toggle()
}) {
Text("Show second view")
}
.sheet(isPresented: $vm.showSecondView) {
SecondView(vm: SecondViewModel(showSecondView: self.$vm.showSecondView))
}
}
}
class FirstViewModel: ObservableObject {
#Published var showSecondView = false
}
struct SecondView: View {
var vm: SecondViewModel
var body: some View {
Button(action: {
self.vm.showSecondView.wrappedValue.toggle()
}) {
Text("Dismiss")
}
}
}
class SecondViewModel {
var showSecondView: Binding<Bool>
init(showSecondView: Binding<Bool>) {
self.showSecondView = showSecondView
}
}

Resources