View using if/else statement displays the wrong page - SwiftUI - if-statement

I am new to SwiftUI and want to display a view based on a boolean value but every time I run my app in the simulator, it always runs the else part and displays ContentView instead of OnboardingView.
Kindly guide what I am doing wrong in the code.
import SwiftUI
#main
struct FirstApp: App {
#AppStorage("isOnboarding") var isOnboarding: Bool = true
var body: some Scene {
WindowGroup {
if isOnboarding {
OnboardingView()
} else
{
ContentView()
}
}
}
}

The code works fine. The "problem" is the persisted value in your device for key "isOnboarding" is false (the default value that you have assigned will work only the first them). To make sure that this statement is right, you can do a simple test: toggle the isOnboarding value and relaunch the app. Something like this:
#main
struct FirstApp: App {
#AppStorage("isOnboarding") var isOnboarding: Bool = true
var body: some Scene {
WindowGroup {
Toggle(isOn: $isOnboarding) {
Text("isOnboarding")
}
if isOnboarding {
OnboardingView()
} else {
ContentView()
}
}
}
}

Related

How do I start my SwiftUI app in a children view?

Okay, here's what my app looks like.
#main
struct MemorizableApp: App {
init() {
FirebaseApp.configure() // Configure the FirebaseApp instance
Database.database().isPersistenceEnabled = true // Set DB persistence to true
}
var body: some Scene {
WindowGroup {
if Auth.auth().currentUser == nil {
SignInView()
.onOpenURL{ url in
GIDSignIn.sharedInstance.handle(url)
}
} else {
MainView()
}
}
}
}
And this is MemorizableApp.swift. As you can see, the app starts in MainView. But I want the app to launch in NotebooksView instead. Obviously, changing MainView to NotebooksView in the code above won't work because 1) NotebooksView doesn't have a NavigationView in it, and 2) I won't be able to navigate back to MainView. But somehow Apple's Shortcuts app did exactly this without creating a custom back button. How can I achieve this behaviour?
What you need to do is activate your NavigationLink programmatically:
In MainView add: #State var shouldNavigate = false, then modify your NavigationLink to the following:
NavigationLink(destination: NoteBooksView(), isActive: $shouldNavigate) {
...
}
And in MemorizableApp:
#main
struct MemorizableApp: App {
init() {
FirebaseApp.configure() // Configure the FirebaseApp instance
Database.database().isPersistenceEnabled = true // Set DB persistence to true
}
var body: some Scene {
WindowGroup {
if Auth.auth().currentUser == nil {
SignInView()
.onOpenURL{ url in
GIDSignIn.sharedInstance.handle(url)
}
} else {
MainView(shouldNavigate: true) //set it to true
}
}
}
}
Note: The use of NavigationLink(destination: Content, isActive: Binding<Bool>) & NavigationView is deprecated in iOS 16. Take a look here & here.

Apply a navigationStyle based on horizontalSizeClass for the root NavigationView and preserve the navigation stack

On app launch, I want to get the horizontalSizeClass and based on if it's compact or regular, apply a navigation style to my root navigation view like so:
import SwiftUI
#main
struct MyApp: App {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some Scene {
WindowGroup {
if sizeClass == .compact {
NavigationView {
Text("Compact size class inside stack navigation style")
}
.navigationViewStyle(StackNavigationViewStyle())
} else {
NavigationView {
Text("Regular size class inside default navigation style")
}
}
}
}
}
However, sizeClass always returns nil in this case.
How do I
determine if the horizontal size class is compact or regular on the root view, and
make the navigation style adapt to the size class any time it changes
My app is targeting iOS 14 for both iPhone and iPad.
Any help or a different approach to adapt for size class changes for the whole app is much appreciated.
Update 1
I tried the suggestions to use a ViewModifier or creating a custom view and adding the navigation in it's body like so:
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MyRootView()
}
}
}
struct MyRootView: View {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
if sizeClass == .compact {
NavigationView {
Text("Compact size class inside stack navigation style")
}
.navigationViewStyle(StackNavigationViewStyle())
} else {
NavigationView {
Text("Regular size class inside default navigation style")
}
}
}
}
However, the navigation stack pops to the root view every time the sizeClass changes. Is there a way to preserve the stack? For example: If the user is 5 levels deep in navigation, and sizeClass changes, change the navigation style while keeping the visible screen?
Thank you!
Update 2
I was able to find a WWDC session explaining exactly what I want, but it's in UIKit.
See 18:35 here: https://developer.apple.com/wwdc20/10105
I'm trying to achieve the same goal in SwiftUI (keep the screen the user selected while changing the size class to compact).
According to the session, UISplitViewController supports this because there's the concept of Restorable and Restore in the detail view. I can't find a way to do this in SwiftUI.
this setup works for me. I read somewhere in the docs that Environment are updated before a view is rendered.
I guess App is not a view.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
if horizontalSizeClass == .compact {
Text("Compact")
} else {
Text("Regular")
}
}
}
Yeah, forgot about the NavigationViews.
You could try something like this (using the code from "https://matteo-puccinelli.medium.com/conditionally-apply-modifiers-in-swiftui-51c1cf7f61d1")
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
extension View {
#ViewBuilder
func ifCondition<TrueContent: View, FalseContent: View>(_ condition: Bool, then trueContent: (Self) -> TrueContent, else falseContent: (Self) -> FalseContent) -> some View {
if condition {
trueContent(self)
} else {
falseContent(self)
}
}
}
struct ContentView: View {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
NavigationView {
if sizeClass == .compact {
Text("Compact size class inside stack navigation style")
} else {
Text("Regular size class inside default navigation style")
}
}
.ifCondition(sizeClass == .compact) { nv in
nv.navigationViewStyle(StackNavigationViewStyle())
} else: { nv in
nv.navigationViewStyle(DefaultNavigationViewStyle())
}
}
}

SwiftUI: Updating ui when view is not present causes "Unable to present view. Please file a bug."

I get the following error: Unable to present view. Please file a bug whenever I make an asynchronous call on a view and leave the view (e.g. navigate to another view in the navigation stack) before it can make changes to the ui. Consequently, the next view in the navigation stack is unable to update its view. How can I fix this problem?
An example of the problem occurring is when I switch from view1 to view2 before my GetIoTThingIndex() call finishes and makes an update to the ui.
GetIoTThingIndex.query(device) { error in
DispatchQueue.main.async { [self] in
...
}
}
EDIT:
After doing more investigating, I found that this problem is due to the fact that I am implementing my logic in an MVVM pattern. When I moved my logic directly into the the view and called the functions and state variables inside the view, everything worked fine. It's interesting because when I started building my app with just a few pages with minimal logic and dependencies, this MVVM pattern worked fine without any bugs. However, when my project grew to 20+ pages with more logic and dependencies, the MVVM pattern causes this bug. Is this just a problem I see or has anyone seen anything like this before and have any recommendations for fixing it?
This is the way I had things with MVVM.
View
struct DeviceView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
Text(viewModel.name)
...
}
}
View Model
class DeviceViewModel: ObservableObject {
#Published var name = ""
public func updateUI() {
...
}
...
}
This is the way I have things now (which works without this bug).
View
struct DeviceView: View {
var body: some View {
Text(name)
...
}
#State var name = ""
public func updateUI() {
...
}
...
}
Are you sure this is what is happening?
I've tested the idea of navigating to another view
before the parent can make a change to its view. And all works well.
This is the code I used for the test, click on the button first, then within 3 sec click on the NavigationLink.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var thingToUpdate = ""
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("text \(thingToUpdate)")
Button("click me first") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
thingToUpdate = " is updated now"
}
}
NavigationLink(destination: Text("the detail view")) {
Text("then to DetailView")
}
}
}
}
}
Edit update using ObservableObject that works for me:
class DeviceViewModel: ObservableObject {
#Published var name = "no name"
public func updateUI() {
// simulated delay on the main thread
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.name = "success"
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("viewModel name is \(viewModel.name)")
Button("click me first") {
viewModel.updateUI()
}
NavigationLink(destination: Text("DetailView")) {
Text("then to DetailView")
}
}
}
}
}

Check User Credentials at Launch (Sign In With Apple) with SwiftUI

I just implemented Sign In With Apple in my SwiftUI App using the examples I found on the web.
Works great but I have a very important question: Where should I do the Check if the user is validated or not?
I have a function checkUserAuth() that returns 3 states: undefined, signedOut and signedIn. Based on each one of those returns, I want to open a different View.
self.signInWithAppleManager.checkUserAuth { (authenticationState) in
switch authenticationState {
case .undefined:
print("AuthenticationState: .undefined")
case .signedOut:
print("AuthenticationState: .signedOut")
case .signedIn:
print("AuthenticationState: .signedIn")
}
}
If my function returns .undefined or .signedOut, I want to open the LoginView.swift. If the function returns .signIn, I want to open the MainView.swift.
Where should I make this Check?
Should I do that on the SceneDelegate.swift and show the right View or should a have a View responsible to make this validation and "navigate" to the right view?
Thanks I hope I could explain my problem.
My fast solution suggests to persist this state in some #Published var and check it in the needed view. It does not looks good, maybe better create checking functions somewhere else (like viewModel), but I think it will direct you to the right way:
import SwiftUI
import Combine
final class SingSingInWithAppleManager: ObservableObject {
#Published var userAuthState: UserAuthInAppleState
init() { // here you can check auth status
userAuthState = .undefined
}
func logIn() {
// here you're making login in and change status
userAuthState = .signedIn
}
}
enum UserAuthInAppleState {
case undefined
case signedOut
case signedIn
}
struct SingInWithAppleExample: View {
#EnvironmentObject var signInWithAppleManager: SingSingInWithAppleManager
var body: some View {
ZStack {
if signInWithAppleManager.userAuthState == .undefined {
Button(action:
{
self.signInWithAppleManager.logIn()
}) {
Text("Login in!")
}
} else {
Text("singed in!")
}
}
}
}
// MARK: how to make it with animation
struct SingInWithAppleExample: View {
#EnvironmentObject var signInWithAppleManager: SingSingInWithAppleManager
var body: some View {
ZStack {
Button(action:
{
withAnimation {
self.signInWithAppleManager.logIn()
}
}) {
Text("Login in!")
}
.opacity(signInWithAppleManager.userAuthState == .undefined ? 1 : 0)
Text("singed in!")
.opacity(signInWithAppleManager.userAuthState == .signedIn ? 1 : 0)
}
}
}
I think you should set this manager in the SceneDelegate func if it's the first thing you want to show. Here is PreviewProvider example:
struct SingInWithAppleExample_Previews: PreviewProvider {
static var previews: some View {
SingInWithAppleExample()
.environmentObject(SingSingInWithAppleManager()) // only add this line of code
}
}
P.S can you share "examples you found on the web" please, soon I'll need to implement it too

Disable drag to dismiss in SwiftUI Modal

I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}