SWIFTUI Binding Not Updating - swiftui

I have a home screen with a “log in” button. Click the button, it will pops up a log in screen. Once it’s logged in, the popover screen will be dismissed. and the home screen will say “logged in!” I have a #State boolean var monitoring button click, and another #EnvironmentObject observing login status. If log in succeeds, isAnon=false . Then the popover window will be dismissed. But it doesn’t work now. when logged in successfully, the popover screen is still on. I’m suspecting the statement Binding<Bool>() doesn’t update when isAnon=false . is this possible ? where did I miss? Thanks a lot! Here is the code
#EnvironmentObject var session: SessionStore
#State var isSignInClicked: Bool = false
var body: some View {
if session.isAnon{// if it is anonymous
Button("Please Sign in to view your activities") {
isSignInClicked.toggle()
}
.popover(isPresented: Binding<Bool>( get: { session.isAnon && isSignInClicked},set: {_ in })){
LogInView()
}
}
else{// if it's logged in
Text("Logged In!")
}
}
}

as #Asperi mentioned, use a #Published for isAnon. This works well for me.
Here is my test code:
import SwiftUI
#main
struct TestApp: App {
#StateObject var session = SessionStore()
var body: some Scene {
WindowGroup {
HomeView().environmentObject(session)
}
}
}
class SessionStore: ObservableObject {
#Published var isAnon = true
}
struct HomeView: View {
#EnvironmentObject var session: SessionStore
#State var isSignInClicked: Bool = false
var body: some View {
if session.isAnon {// if it is anonymous
Button("Please Sign in to view your activities") {
isSignInClicked.toggle()
}
.popover(isPresented: Binding<Bool>( get: { session.isAnon && isSignInClicked},set: {_ in })){
LogInView()
}
}
else {// if it's logged in
Text("Logged In!")
}
}
}
EDIT1: include the missing test LoginView
struct LogInView: View {
#EnvironmentObject var session: SessionStore
var body: some View {
Text("LogInView here")
.onDisappear {
session.isAnon = false
}
// alternative using a button
// Button("LogInView click me") {
// session.isAnon = false
// }
}
}

Related

How to get NavigationStack to retain NavigationPath state when switching views?

I modified an app to use the new NavigationSplitView and NavigationStack, but I can't figure out how to have the NavigationPath retain the state when it's not the active view.
Below is some sample code. I run it on an iPad in landscape mode, or on a Mac (Designed for iPad). It starts with View1 selected and displays View1 in the details. I then tap on SubView and it pushes to SubView. If I then tap on View2, View2 is displayed in the details. If I tap on View1 again, View1 is back at it's root, and is no longer pushed to the SubView. How can I fix this so that when I go back to View1 it is still pushed to SubView?
import SwiftUI
struct ViewType: Identifiable, Hashable {
let id: String
}
private var viewTypes = [
ViewType(id: "View1"),
ViewType(id: "View2"),
]
struct ContentView: View {
#StateObject private var navigationModel = NavigationModel()
#State var selection: Set<String> = [viewTypes[0].id]
var body: some View {
NavigationSplitView {
List(viewTypes, selection: $selection) { viewType in
Text("\(viewType.id)")
}
} detail: {
switch selection.first ?? "Unknown" {
case "View1":
View1()
case "View2":
View2()
default:
Text("Unknown")
}
}
.navigationTitle(selection.first ?? "Unknown")
.environmentObject(navigationModel)
}
}
struct View1: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationStack(path: $navigationModel.path) {
Text("View1")
NavigationLink("SubView", value: "SubView")
.navigationDestination(for: String.self) { name in
Text(name)
.onAppear {
print((navigationModel.path.count))
}
}
}
}
}
struct View2: View {
var body: some View {
NavigationStack() {
Text("View2")
}
}
}
final class NavigationModel: ObservableObject {
#Published var path = NavigationPath() {
didSet {
print("path.count: \(path.count)")
}
}
}

SwiftUI iOS 16, close multiple modals not working

I'm trying to present multiple modals on top of my home view, but when I try to dismiss all modals, there is only the first one that close...
(I know, there is a lot of subject about this, but I didn't found any solution that was working for me...)
Any ideas?
Here is my testing code:
struct ContentView: View {
#State var presentA = false
var body: some View {
Button("Present A") { presentA = true }
.sheet(isPresented: $presentA) { ContentViewA(presentAll: $presentA) }
}
}
struct ContentViewA: View {
#Binding var presentAll: Bool
#State var presentB = false
var body: some View {
Button("Present B") { presentB = true }
.sheet(isPresented: $presentB) { ContentViewB(presentAll: $presentAll) }
}
}
struct ContentViewB: View {
#Binding var presentAll: Bool
var body: some View {
Button("Close all") {
presentAll = false
}
}
}
So when I touch the "Close all" button, I go back to the ContentViewA instead of the ContentView...
In my memory this was working with the previous version of SwiftUI but it seems that's not working anymore...
What am I doing wrong?
I don't think it is a valid user flow and SwiftUI does not handle it. The possible workaround is the same as for UIKit
Tested with Xcode 14b3 / iOS 16
var body: some View {
Button("Close all") {
UIApplication.shared.keyWindow?
.rootViewController?
.dismiss(animated: false, completion: nil) // false is important !!
}
}

SwiftUI State Variable not Rendering View Unless Used Inside View (verses in modifier)

Sorry for terrible wording, but in onAppear I am changing a State variable, however this does not re-render the view unless I use that State variable somewhere inside the view.
I am trying to pop a modal if the user is not logged in, but it won't pop unless I put the variable in the view like shown below
struct SearchView: View {
// Environment Variables
#EnvironmentObject var session: SessionStore
#State var sheetIsPresented: Bool = false
var body: some View {
NavigationView {
// Just having a line like this will cause it to work
if self.sheetIsPresented || !self.sheetIsPresented {}
}
.onAppear {
if self.session.userSession != nil {
self.sheetIsPresented = true
}
}
.sheet(isPresented: $sheetIsPresented) {
WelcomeSignInModal(sheetIsPresented: self.$sheetIsPresented)
}
}
}
You can try the following demo:
class SessionStore: ObservableObject {
var userSession: String? = "session"
}
struct ContentView: View {
#EnvironmentObject var session: SessionStore
#State var sheetIsPresented: Bool = false
var body: some View {
NavigationView {
Text("SearchView")
}
.onAppear {
self.sheetIsPresented = self.session.userSession != nil
}
.sheet(isPresented: $sheetIsPresented) {
Text("WelcomeSignInModal")
}
}
}
Tested with Xcode 11.6, iOS 13.6

How can I show a page depending of child button clicked with SwiftUI?

I am trying to rewrite my app using SwiftUI only and I am having difficulty with the EnvironmentObject, trying to understand how it works…
I want to redirect my app users to the appropriate page at launch, depending on:
if this is their first time
if they have a login,
if they want to start using without login
If it is the first time the app is launched, LocalStorage has no data so I present the app on a welcome page
I offer the choice of 2 buttons to click on:
“New User” which redirect to the main page of the app and create a new user
“Login” which present the login page to retrieve the last backup
If the app has previously been launched, I present the main page straight away.
Now said, if I initiate my “currentPage” as “MainView” or “LoginView”, it works - but NOT if it is set as “WelcomeView”.
I presume the problem comes when the variable gets changed from a subview? I thought the use of #EnvironmentObject was the way to get around this…
Can someone explain to me how it works?
My various files are:
import SwiftUI
import Combine
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "WelcomeView" {
didSet {
objectWillChange.send(self)
}
}
}
import SwiftUI
struct ParentView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "WelcomeView" {
WelcomeView()
}
else if viewRouter.currentPage == "MainView" {
MainView()
}
else if viewRouter.currentPage == "LoginView" {
LoginView()
}
}
}
}
import SwiftUI
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons().environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}
import SwiftUI
struct LoginButtons: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {
self.viewRouter.currentPage = "MainView"
}) {
Text("NEW USER")
}
Button(action: {
self.viewRouter.currentPage = "LoginView"
}) {
Text("I ALREADY HAVE AN ACCOUNT")
}
}
}
}
import SwiftUI
struct MainView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
import SwiftUI
struct LoginView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
Many Thanks in advance! :wink:
Ok so in your main view, the one that you are going to decide where to send your user, you could check for the app if it was lunched before or not, depending on that do whatever you want. Once you know how to do this, you can adapt to the other things. This is how you can check for it, again, in your main view router:
init() {
// Create initial Data if not data has been setup
if (InitialAppSetup().initialDataLoaded == false) {
InitialAppSetup().createInitialData()
}
// Onboarding screen
if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
UserDefaults.standard.set(true, forKey: "didLaunchBefore")
currentPage = "onboardingView"
} else {
currentPage = "homeView"
}
}
The InitialAppSetup() class has a UserDefault which goes like this:
#Published var initialDataLoaded: Bool = UserDefaults.standard.bool(forKey: "InitialData") {
didSet {
UserDefaults.standard.set(self.initialDataLoaded, forKey: "InitialData")
}
}
Ok... My 'mistake' was to add an extra ".environmentObject(ViewRouter())" when calling my subview "LoginButtons".
If I remove it, it works!.. But why?!?
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons()
// --> .environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}

SwiftUI - View not refreshing when a property has changed

I'd appreciate any help with this, I'm just a beginner so I'm not sure if I'm misunderstanding, or have the implementation wrong or if this is a bug.
I'm trying to have my MacOS app segue to the main screen after a successful login. I have an appState to share state with the rest of the app. The AppState is a class conforming to Observable object and I added an observer to print whenever the isLoggedIn property changes:
class AppState : ObservableObject {
#Published var isLoggedIn = false {
didSet {
print("AppState isLoggedin: \(isLoggedIn)")
}
}
}
I also have a MasterView struct to deal with changing the main view.
struct MasterView: View {
#ObservedObject var appState: AppState = AppState()
var body: some View {
return Group {
if appState.isLoggedIn {
NavView()
} else {
LoginView()
}
}.frame(width: 1200, height: 800)
}
}
I have a bunch of code to handle doing the login which I won't post for the sake of brevity, suffice to say that it works, isLoggedIn is set to true and prints to the console after a successful login. The issue is that the view never updates to reflect this so I'm still stuck on the login screen.
Any help is greatly appreciated, I've spent more time on this than I care to admit. Thanks!
Update: I remember having trouble with #EnvironmentObject and so I switched to #ObservableObject and #Published. After re-implementing #EnvironmentObject I now remember why: I have a networking class which causes a crash as it is not an ancestor view. Per Paul Hudson's comment, "Note: Environment objects must be supplied by an ancestor view – if SwiftUI can’t find an environment object of the correct type you’ll get a crash. This applies for previews too, so be careful."
For More Information.
I figured it out, working code below.
AppState:
final class AppState : ObservableObject {
#Published var isLoggedIn = false {
didSet {
print("AppState isLoggedIn: \(isLoggedIn)")
}
}
}
Content View :
struct ContentView: View {
#ObservedObject var appState: AppState
var body: some View {
return Group {
if appState.isLoggedIn {
MainView(appState: appState)
} else {
LoginView(appState: appState)
}
}.frame(maxWidth: 1200, maxHeight: 800)
}
}
Login View:
struct LoginView: View {
#ObservedObject var appState: AppState
var body: some View {
Button(action: {
withAnimation {
self.appState.isLoggedIn.toggle()
}
}) {
Text("Go to Main View")
}.padding()
}
}
Finally, Main View:
struct MainView: View {
#ObservedObject var appState: AppState
var body: some View {
Button(action: {
withAnimation {
self.appState.isLoggedIn.toggle()
}
}) {
Text("Back To Login View")
}.padding()
}
}