NavigationStack pushing new View issue with #Published - ios

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
}
}
}

Related

How to pass data between ViewModels in SwiftUI

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.

Swift UI updating #EnvironmentObject prevent other update on #StateObject

I have the following view ModalView opened by by a parent view ParentView using a button in the toolbar
** Parent View **
import SwiftUI
struct ParentView: View {
#EnvironmentObject var environmentObject: MainStore
#State private var showCreationView = false
var body: some View {
NavigationView {
Text("ENV_OBJ Count: \(environmentObject.shoppingChartFullList.count)")
.navigationBarTitle(Text("Navigation Bar Title"))
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(action: { showCreationView = true }) {
Image(systemName: "plus")
}
.sheet(isPresented: $showCreationView, content: {
ModalView(showModelView: $showCreationView)
})
}
}
}
}
}
struct ParentView_Previews: PreviewProvider {
static var previews: some View {
ParentView().environmentObject(MainStore())
}
}
** Modal View **
import SwiftUI
struct ModalView: View {
#EnvironmentObject var environmentObject: MainStore
#Binding var showModelView: Bool
#State var newItem = ShoppingChartModel()
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Button(action: {
environmentObject.shoppingChartFullList.append(newItem)
self.showModelView = false
}) {Text("Salva")}
}
}
struct ModalView_Previews: PreviewProvider {
static var previews: some View {
ModalView(showModelView: .constant(true)).environmentObject(MainStore())
}
}
** Main Store **
import Foundation
import SwiftUI
import Combine
final class MainStore: ObservableObject {
//An observable object needs to publish any changes to its data, so that its subscribers can pick up the change.
#Published var shoppingChartFullList: [ShoppingChartModel] = load()
}
The problem is that, action executed by the Save Button in the Modal View doesn't dismiss the modal (even if it correctly updates both the Boolean variable and the Env_Obj).
Seems to be related with the toolbar... in fact if I remove the Navigation View and the toolbar, putting the button directly in a stack ... it works...
In the Parent2View I remove the NavigationView and just configured the button directly in the view after the Text property. And it is working as expected.
import SwiftUI
struct ParentView2: View {
#EnvironmentObject var environmentObject: MainStore
#State private var showCreationView = false
var body: some View {
VStack {
Text("ENV_OBJ Count: \(environmentObject.shoppingChartFullList.count)")
Button(action: { showCreationView = true }) {
Image(systemName: "plus")
}
.sheet(isPresented: $showCreationView, content: {
ModalView(showModelView: $showCreationView)
})
}
}
}
struct ParentView2_Previews: PreviewProvider {
static var previews: some View {
ParentView2().environmentObject(MainStore())
}
}
UPDATE
Ok I finally found the solution. The issue is the toolbar. If the ParentView is modified as follow, all start working well
struct ParentView: View {
#EnvironmentObject var environmentObject: MainStore
#State private var showCreationView = false
var body: some View {
NavigationView {
Text("ENV_OBJ Count: \(environmentObject.shoppingChartFullList.count)")
.navigationBarTitle("Nav View Title")
.navigationBarItems(trailing:
Button(action: {
self.showCreationView.toggle()
})
{
Image(systemName: "plus")
.font(Font.system(.title))
}
)
}
.sheet(isPresented: $showCreationView) {
ModalView(showModelView: $showCreationView)
}
}
}
Thanks!
Cristian

Passing List between Views in SwiftUi

I'm making a ToDo list app on my own to try to get familiar with iOS development and there's this one problem I'm having:
I have a separate View linked to enter in a new task with a TextField. Here's the code for this file:
import SwiftUI
struct AddTask: View {
#State public var task : String = ""
#State var isUrgent: Bool = false // this is irrelevant to this problem you can ignore it
var body: some View {
VStack {
VStack(alignment: .leading) {
Text("Add New Task")
.bold()
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
TextField("New Task...", text: $task)
Toggle("Urgent", isOn: $isUrgent)
.padding(.vertical)
Button("Add task", action: {"call some function here to get what is
in the text field and pass back the taskList array in the Content View"})
}.padding()
Spacer()
}
}
}
struct AddTask_Previews: PreviewProvider {
static var previews: some View {
AddTask()
}
}
So what I need to take the task variable entered and insert it into the array to be displayed in the list in my main ContentView file.
Here's the ContentView file for reference:
import SwiftUI
struct ContentView: View {
#State var taskList = ["go to the bakery"]
struct AddButton<Destination : View>: View {
var destination: Destination
var body: some View {
NavigationLink(destination: self.destination) { Image(systemName: "plus") }
}
}
var body: some View {
VStack {
NavigationView {
List {
ForEach(self.taskList, id: \.self) {
item in Text(item)
}
}.navigationTitle("Tasks")
.navigationBarItems(trailing: HStack { AddButton(destination: AddTask())})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
I need a function to take the task entered in the TextField, and pass it back in the array in the ContentView to be displayed in the List for the user
-thanks for the help
You can add a closure property in your AddTask as a callback when the user taps Add Task. Just like this:
struct AddTask: View {
var onAddTask: (String) -> Void // <-- HERE
#State public var task: String = ""
#State var isUrgent: Bool = false
var body: some View {
VStack {
VStack(alignment: .leading) {
Text("Add New Task")
.bold()
.font(.title)
TextField("New Task...", text: $task)
Toggle("Urgent", isOn: $isUrgent)
.padding(.vertical)
Button("Add task", action: {
onAddTask(task)
})
}.padding()
Spacer()
}
}
}
Then, in ContentView:
.navigationBarItems(
trailing: HStack {
AddButton(
destination: AddTask { task in
taskList.append(task)
}
)
}
)
#jnpdx provides a good solution by passing Binding of taskList to AddTask. But I think AddTask is used to add a new task so it should only focus on The New Task instead of full taskList.

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)!])
}
}
}
}

Resources