view automatically pop to previous view in swift ui due to view model is observed from tabview to inside multiple view? - swiftui

I am facing an issue while pushing 1 view to another view. Let me explain the hierarchy.
ContentView -> 2 tabs, TabAView and TabBView
Inside TabBView. There is 1 view used ConnectView: Where is a Button to connect. After tapping on the button of Connect, the user move to another View which is called as UserAppView.
Here is the code to understand better my problem.
ContentView.swift
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
#ObservedObject var userViewModel: UserViewModel
#ObservedObject var userAppViewModel: UserAppViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(userViewModel: userViewModel, userAppViewModel: userAppViewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
TabBView(userViewModel: userViewModel, userAppViewModel: userAppViewModel)
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var userViewModel: UserViewModel
#ObservedObject var userAppViewModel: UserAppViewModel
#EnvironmentObject var settings: Settings
init(userViewModel: UserViewModel, userAppViewModel: UserAppViewModel) {
self.userViewModel = userViewModel
self.userAppViewModel = userAppViewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is another TabBView:
struct TabBView: View {
#ObservedObject var userViewModel: UserViewModel
#ObservedObject var userAppViewModel: UserAppViewModel
init(userViewModel: UserViewModel, userAppViewModel: UserAppViewModel) {
self.userViewModel = userViewModel
self.userAppViewModel = userAppViewModel
}
var body: some View {
VStack (spacing: 10) {
NavigationLink(destination: ConnectView(viewModel: ConnectViewModel(id: id!), userViewModel: userViewModel, userAppViewModel: userAppViewModel)) {
UserCardWidget()
}
}
}
}
There is 1 connectView used on the TabBView through which the user will connect. ConnectViewModel is used here to call connect API.
class ConnectViewModel: ObservableObject {
var id: String?
init(id: String) {
self.id = id
}
func connect(completion: #escaping () -> Void) {
APIService.shared.connectApp(id: self.id!) { connected in
DispatchQueue.main.async {
self.isConnected = connected ?? false
completion()
}
}
}
}
This is connect view
struct ConnectView: View {
#ObservedObject var connectViewModel: ConnectViewModel
#ObservedObject var userViewModel: UserViewModel
#ObservedObject var userAppViewModel: UserAppViewModel
#State var buttonTitle = "CONNECT WITH THIS"
#State var isShowingDetailView = false
var body: some View {
VStack {
Spacer()
if let id = connectViewModel.id {
NavigationLink(destination: UserAppView(id: id, userAppViewModel: userAppViewModel, userViewModel: userViewModel), isActive: $isShowingDetailView) {
Button(buttonTitle, action: {
connectViewModel.connect {
buttonTitle = "CONNECTED"
isShowingDetailView = true
}
})
}
}
}
}
}
This is the UserAppViewModel where API is hit to fetch some user-related details:
class UserAppViewModel: ObservableObject {
var id = ""
func getdetails() {
APIService.shared.getDetails() { userDetails in
DispatchQueue.main.async {
/// code
}
}
}
}
This is UserAppView class
struct UserAppView: View {
#ObservedObject var userViewModel: UserViewModel
#ObservedObject var userAppViewModel: UserAppViewModel
init(id: String, userAppViewModel: UserAppViewModel, userViewModel: UserViewModel) {
self.id = id
self.userAppViewModel = userAppViewModel
self.userViewModel = userViewModel
}
var body: some View {
VStack {
Text(userAppViewModel.status)
}.onAppear(perform: {
**userAppViewModel.getDetails**(id: id)
})
}
}
Whenever this statement userAppViewModel.getDetails executes, the user comes to UserAppView but when the response of getDetails is received, it automatically goes to ConnectView.
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.

Related

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

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.

How to pop to specific view in the TabView Application in swiftui. I used StackNavigation also but not working in swiftui

I am facing an issue while popping to a specific view. Let me explain the hierarchy.
ContentView -> 2 tabs, TabAView and TabBView
Inside TabBView. There is 1 view used ConnectView: Where is a Button to connect. After tapping on the button of Connect, the user move to another View which is called as UserAppView. From Here User can check his profile and update also. After the Update API call, need to pop to UserAppView from UserFirstFormView.
Here is the code to understand better my problem.
ContentView.swift
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
#ObservedObject var userViewModel: UserViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(userViewModel: userViewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
TabBView(userViewModel: userViewModel)
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var userViewModel: UserViewModel
#EnvironmentObject var settings: Settings
init(userViewModel: UserViewModel) {
self.userViewModel = userViewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is another TabBView:
struct TabBView: View {
#ObservedObject var userViewModel: UserViewModel
init(userViewModel: UserViewModel) {
self.userViewModel = userViewModel
}
var body: some View {
VStack (spacing: 10) {
NavigationLink(destination: ConnectView(viewModel: ConnectViewModel(id: id!), userViewModel: userViewModel)) {
UserCardWidget()
}
}
}
}
There is 1 connectView used on the TabBView through which the user will connect. ConnectViewModel is used here to call connect API.
class ConnectViewModel: ObservableObject {
var id: String?
init(id: String) {
self.id = id
}
func connect(completion: #escaping () -> Void) {
APIService.shared.connectApp(id: self.id!) { connected in
DispatchQueue.main.async {
self.isConnected = connected ?? false
completion()
}
}
}
}
This is connect view
struct ConnectView: View {
#ObservedObject var connectViewModel: ConnectViewModel
#ObservedObject var userViewModel: UserViewModel
#State var buttonTitle = "CONNECT WITH THIS"
#State var isShowingDetailView = false
var body: some View {
VStack {
Spacer()
if let id = connectViewModel.id {
NavigationLink(destination: UserAppView(id: id, userViewModel: userViewModel), isActive: $isShowingDetailView) {
Button(buttonTitle, action: {
connectViewModel.connect {
buttonTitle = "CONNECTED"
isShowingDetailView = true
}
})
}
}
}
}
}
This is the UserAppViewModel where API is hit to fetch some user-related details:
class UserAppViewModel: ObservableObject {
var id = ""
func getdetails() {
APIService.shared.getDetails() { userDetails in
DispatchQueue.main.async {
/// code
}
}
}
}
This is UserAppView class
struct UserAppView: View {
#ObservedObject var userViewModel: UserViewModel
#State private var signUpInButtonClicked: Bool = false
#StateObject private var userAppViewModel = UserAppViewModel()
init(id: String, userViewModel: UserViewModel) {
self.id = id
self.userViewModel = userViewModel
}
var body: some View {
VStack {
Text(userAppViewModel.status)
VStack {
Spacer()
NavigationLink(
destination: ProfileView(userAppViewModel: userAppViewModel, isActive: $signUpInButtonClicked)) { EmptyView() }
if /// Condition {
Button(action: {
signUpInButtonClicked = true
}, label: {
ZStack {
/// code
}
.frame(maxWidth: 77, maxHeight: 25)
})
}
}.onAppear(perform: {
**userAppViewModel.getDetails**(id: id)
})
}
}
From Here, the User Can Navigate to ProfileView.
struct ProfileUpdateView: View {
#State private var navigationSelectionFirstFormView = false
#State private var navigationSelectionLastFormView = false
public var body: some View {
VStack {
NavigationLink(destination: UserFirstFormView(userAppViewModel: userAppViewModel), isActive: $navigationSelectionFirstFormView) {
EmptyView()
}
NavigationLink(destination: UserLastFormView(userAppViewModel: userAppViewModel), isActive: $navigationSelectionLastFormView) {
EmptyView()
}
}
.navigationBarItems(trailing: Button(action: {
if Condition {
navigationSelectionFirstFormView = true
} else {
navigationSelectionLastFormView = true
}
}, label: {
HStack {
Text("Action")
.foregroundColor(Color.blue)
}
})
)
}
}
Further, their user will move to the next screen to update the profile.
struct UserFirstFormView: View {
var body: some View {
VStack {
/// code
///
Button("buttonTitle", action: {
API Call completion: { status in
if status {
self.rootPresentationMode.wrappedValue.dismiss()
}
})
})
.frame(maxHeight: 45)
}
}
}
I am trying to pop from this view once the API response is received but nothing is working.
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.
You could use the navigation link with, tag: and selection: overload and let the viewmodel control what link is open, here a example
enum profileViews {
case view1
case view2}
in your viewModel add an published var that will hold the active view
#Published var activeView: profileViews?
then in your navigation link you can do it like this
NavigationLink(
destination: secondView(profileViewModel: ProfileViewModel ),
tag: profileViews.view1,
selection: self.$profileViewModel.activeView
){}
Then you could pop any view just updating the variable inside the view model
self.profileViewModel.activeView = nil

How to dismiss sheet from within NavigationLink

In the following example, I have a view that shows a sheet of ViewOne. ViewOne has a NavigationLink to ViewTwo.
How can I dismiss the sheet from ViewTwo?
Using presentationMode.wrappedValue.dismiss() navigates back to ViewOne.
struct ContentView: View {
#State private var isShowingSheet = false
var body: some View {
Button("Show sheet", action: {
isShowingSheet.toggle()
})
.sheet(isPresented: $isShowingSheet, content: {
ViewOne()
})
}
}
struct ViewOne: View {
var body: some View {
NavigationView {
NavigationLink("Go to ViewTwo", destination: ViewTwo())
.isDetailLink(false)
}
}
}
struct ViewTwo: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss sheet here") {
presentationMode.wrappedValue.dismiss()
}
}
}
This may depend some on platform -- in a NavigationView on macOS, for example, your existing code works.
Explicitly passing a Binding to the original sheet state should work:
struct ContentView: View {
#State private var isShowingSheet = false
var body: some View {
Button("Show sheet", action: {
isShowingSheet.toggle()
})
.sheet(isPresented: $isShowingSheet, content: {
ViewOne(showParentSheet: $isShowingSheet)
})
}
}
struct ViewOne: View {
#Binding var showParentSheet : Bool
var body: some View {
NavigationView {
NavigationLink("Go to ViewTwo", destination: ViewTwo(showParentSheet: $showParentSheet))
//.isDetailLink(false)
}
}
}
struct ViewTwo: View {
#Binding var showParentSheet : Bool
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss sheet here") {
showParentSheet = false
}
}
}

How can I have multiple instance of a Class/Model in SwiftUI?

The first part of question is answered. Let's elaborate this example to:
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// leads to that I create a new viewModel every time, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
My problem is now that when I type something into the TextField and press the return button on the keyboard the text is removed.
This is the most strange way of coding that i seen, how ever I managed to make it work:
I would like say that you can use it as leaning and testing, but not good plan for real app, How ever it was interesting to me to make it working.
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
class CreateNewCardViewModel: ObservableObject, Identifiable, Equatable {
init(_ id: Int) {
self.id = id
}
#Published var id: Int
#Published var definition: String = ""
#Published var show = false
static func == (lhs: CreateNewCardViewModel, rhs: CreateNewCardViewModel) -> Bool {
return lhs.id == rhs.id
}
}
let arrayOfModel: [CreateNewCardViewModel] = [ CreateNewCardViewModel(0), CreateNewCardViewModel(1), CreateNewCardViewModel(2),
CreateNewCardViewModel(3), CreateNewCardViewModel(4), CreateNewCardViewModel(5),
CreateNewCardViewModel(6), CreateNewCardViewModel(7), CreateNewCardViewModel(8),
CreateNewCardViewModel(9) ]
struct ReadModelView: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
struct MainView: View {
#State private var arrayOfModelState = arrayOfModel
#State private var showModel: Int?
#State private var isPresented: Bool = false
var body: some View {
VStack {
ForEach(Array(arrayOfModelState.enumerated()), id:\.element.id) { (index, item) in
Button(action: { showModel = index; isPresented = true }, label: { Text("Show Model " + item.id.description) }).padding()
}
if let unwrappedValue: Int = showModel {
Color.clear
.sheet(isPresented: $isPresented, content: { ReadModelView(viewModel: arrayOfModelState[unwrappedValue]) })
}
}
.padding()
}
}

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