I'm using #EnvironmentObject in SwiftUI and I got this "View.environmentObject(_:) for ViewModel may be missing as an ancestor of this view" Error - swiftui

My code is something like this:
class ViewModel: ObservableObject {
#Published var value = ""
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.userSession != nil {
MyTabView()
} else {
LoginView()
}
}
.environmentObject(viewModel)
}
}
struct MyTabView: View {
var body: some View {
TabView {
View1()
.tabItem{}
View2()
.tabItem{}
View3()
.tabItem{}
View4()
.tabItem{}
}
}
}
struct View4: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
NavigationLink(destination: EditView().environmentObject(viewModel)){
Text("Edit")
}
}
}
}
struct EditView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
if viewModel.value != "" { //this is where I get the error
Text("\(viewModel.value)")
}
}
}
I've tried putting the environmentObject at MyTabView() in ContentView()
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.userSession != nil {
MyTabView().environmentObject(viewModel)
} else {
LoginView()
}
}
}
}
I've tried putting the environmentObject at NavigationView in View4()
struct View4: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
NavigationLink(destination: EditView()){
Text("Edit")
}
}.environmentObject(viewModel)
}
}
The value from ViewModel is not getting passed into the EditView. I have tried many solutions I can find but non of those are helping with the error.
Can anyone please let me know what have I done wrong?

Here is the test code I used (entirely based on yours) that shows
"...The value from ViewModel is getting passed into the EditView...".
Unless I missed something, the code you provide does not reproduce the error you show.
class ViewModel: ObservableObject {
#Published var value = ""
#Published var userSession: String? // <-- for testing
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.userSession != nil {
MyTabView()
} else {
LoginView()
}
}.environmentObject(viewModel)
}
}
struct MyTabView: View {
var body: some View {
TabView {
Text("View1").tabItem{Text("View1")}
Text("View2").tabItem{Text("View2")}
Text("View3").tabItem{Text("View3")}
View4().tabItem{Text("View4")}
}
}
}
struct View4: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
NavigationLink(destination: EditView().environmentObject(viewModel)){
Text("Edit")
}
}
}
}
struct EditView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
if viewModel.value != "" { // <-- here no error
Text(viewModel.value) // <-- here viewModel.value is a String
}
}
}
struct LoginView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
Button("Click me", action: {
viewModel.userSession = "something" // <-- to trigger the if in ContentView
viewModel.value = "testing-4-5-6" // <-- here change the value
})
}
}
Try this code and let us know if you get the error you show.

Related

SwiftUI Control Flow and ViewBuilder

I want to have a mother view that displays a different child view based on a #State bool.
However I get two errors when doing that.
On the start of the body closure:
Struct 'ViewBuilder' requires that 'EmptyCommands' conform to 'View'
Return type of property 'body' requires that 'EmptyCommands' conform
to 'View'
And inside the control flow statement:
Closure containing control flow statement cannot be used with result
builder 'CommandsBuilder'
struct ResultView: View {
#State var resultViewSuccess = false
let resultViewModel: ResultViewModel
var body: some View {
Group {
if let showresultView = resultViewSuccess {
ViewOne(viewModel: resultViewModel)
} else {
ViewTwo(
resultViewSuccess: $resultViewSuccess,
viewModel: resultViewModel,
)
}
}
}
}
struct ViewTwo: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
NavigationView {
ButtonResult(
resultViewSuccess: $resultViewSuccess,
viewModel: viewModel)
}
}
struct ButtonResult: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
Button(action: {
self.resultViewSuccess = true
}) {
Text("View Results")
}
}
}

SwiftUI: .listRowBackground is not updated when new item added to the List

I'm trying to find the reason why .listRowBackground is not updated when a new item has been added to the list. Here is my code sample:
#main
struct BGtestApp: App {
#ObservedObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView().environmentObject(vm)
} label: {
Text(item)
}
.listRowBackground(Color.yellow)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
Button("Add new") {
vm.items.append("Ananas")
}
}
}
How it looks like:
TIA for you help!
You can force the list to refresh when you cone back to the list. You can tag an id for your list by using .id(). Here is my solution:
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
#State private var viewID = UUID()
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView()
.environmentObject(vm)
} label: {
Text(item)
}
}
.listRowBackground(Color.yellow)
}
.id(viewID)
.onAppear {
viewID = UUID()
}
}
}
}
Hope it's helpful for you.
The solution I found is not ideal, but should work. What I did is made items to be #State variable :
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
#State var items: [String] = []
var body: some View {
NavigationView {
VStack {
List {
ForEach(items, id: \.self) { item in
NavigationLink {
DetailView().environmentObject(vm)
} label: {
RowView(item: item)
}
.listRowBackground(Color.yellow)
}
}
.onAppear {
self.items = vm.items
}
}
}

How can I navigate to a detail view of an item by using an #EnvironmentObject to route the views?

I have the following code in SwiftUI. I am expecting it to navigate from the list view to the PetView() with the proper name showing when tapping on one of the items in the ForEach loop or the button that says "Go to first pet". However, when I tap on an item or the button, the app doesn't do anything. What am I doing wrong? Thank you for your help!
import SwiftUI
#main
struct TestListAppApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(ViewRouter())
}
}
}
import SwiftUI
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ForEach(viewRouter.pets) { pet in
NavigationLink(
destination: PetView(),
tag: pet,
selection: $viewRouter.selectedPet,
label: {
Text(pet.name)
}
)
}
Button("Go to first pet.") {
viewRouter.selectedPet = viewRouter.pets[0]
}
}
}
import Foundation
class ViewRouter: ObservableObject {
#Published var selectedPet: Pet? = nil
#Published var pets: [Pet] = [Pet(name: "Louie"), Pet(name: "Fred"), Pet(name: "Stanley")]
}
import SwiftUI
struct PetView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
Text(viewRouter.selectedPet!.name)
}
}
import Foundation
struct Pet: Identifiable, Hashable {
var name: String
var id: String { name }
}
try this:
#main
struct TestListAppApp: App {
#StateObject var viewRouter = ViewRouter()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewRouter)
}
}
}
struct PetView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
if let pet = viewRouter.selectedPet {
Text(pet.name)
} else {
EmptyView()
}
}
}
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
NavigationView {
List {
ForEach(viewRouter.pets) { pet in
NavigationLink(destination: PetView(),
tag: pet,
selection: $viewRouter.selectedPet,
label: {
Text(pet.name)
})
}
Button("Go to first pet.") {
viewRouter.selectedPet = viewRouter.pets[0]
}
}
}
}
}

Missing argument for parameter 'View Call' in call

I am struggle with understanding about why i have to give Popup view dependency named vm while calling this view since it is observable
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView() /// this line shows error
}
}
}
struct DetailView:View {
#ObservedObject var vm:ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
You have to set your vm property when you init your View. Which is the usual way.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView(vm: ViewModel()) // Initiate your ViewModel() and pass it as DetailView() parameter
}
}
}
struct DetailView:View {
var vm: ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
Or you could use #EnvironmentObject. You have to pass an .environmentObject(yourObject) to the view where you want to use yourObject, but again you'll have to initialize it before passing it.
I'm not sure it's the good way to do it btw, as an environmentObject can be accessible to all childs view of the view you declared the .environmentObject on, and you usually need one ViewModel for only one View.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView().environmentObject(ViewModel()) // Pass your ViewModel() as an environmentObject
}
}
}
struct DetailView:View {
#EnvironmentObject var vm: ViewModel // you can now use your vm, and access it the same say in all childs view of DetailView
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}

How to swap my #State of my SwiftUI view for my view model #Published variable?

I have a button that triggers my view state. As I have now added a network call, I would like my view model to replace the #State with its #Publihed variable to perform the same changes.
How to use my #Published in the place of my #State variable?
So this is my SwiftUI view:
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
// This is the value I want to use as #Publisher
#State var isLoggedIn = false
var body: some View {
ZStack {
Button(action: {
// Before my #State was here
// self.isLoggedIn = true
self.viewModel.login()
}) {
Text("Log in")
}
if isLoggedIn {
TutorialView()
}
}
}
}
And this is my model:
final class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
private var subscriptions = Set<AnyCancellable>()
func demoLogin() {
AuthRequest.shared.login()
.sink(
receiveCompletion: { print($0) },
receiveValue: {
// My credentials
print("Login: \($0.login)\nToken: \($0.token)")
DispatchQueue.main.async {
// Once I am logged in, I want this
// value to change my view.
self.isLoggedIn = true } })
.store(in: &subscriptions)
}
}
Remove state and use view model member directly, as below
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
self.viewModel.demoLogin()
}) {
Text("Log in")
}
if viewModel.isLoggedIn { // << here !!
TutorialView()
}
}
}
}
Hey Roland I think that what you are looking for is this:
$viewMode.isLoggedIn
Adding the $ before the var will ensure that SwiftUI is aware of its value changes.
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
viewModel.login()
}) {
Text("Log in")
}
if $viewMode.isLoggedIn {
TutorialView()
}
}
}
}
class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
func login() {
isLoggedIn = true
}
}