Full Screen Cover Flickers when IOS app is opened - swift

So basically in my SwiftUI app, there is a login screen and if you are already logged in then you move on to the HomeView(). If not, then you stay on the LoginView(). However, every time I open the app, the .fullScreenCover flickers before the .onAppear{} statement realizes it's time for the cover to disappear. Here is the code:
struct HomeView: View {
#ObservedObject var fireViewModel = FirebaseViewModel()
#State var loginPresented = true
var body: some View {
ZStack {
VStack {
Text("You are already Signed in")
Button(action: {
fireViewModel.signOut()
}, label: {
Text("Sign Out")
})
}
}
.onAppear {
if fireViewModel.signedIn {
loginPresented = true
} else {
loginPresented = false
}
}
.fullScreenCover(isPresented: $loginPresented, onDismiss: nil, content: {
LoginView()
})
}
}

Setting the
#State var loginPresented = false
In the beginning solves the problem.

Related

How to display Alert dialog on MacOS using SwiftUI

How can I display an alert dialog box from a menu item for MacOS apps using SwiftUI?
The usual code which works for iOS #State var isOn = false and .alert("title", isPresented: isOn) {..} doesn't work.
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
CommandMenu("Test menu") {
Button(action: {
// I want to show an alert dialog dialog here.
}) {
Text("Click Me")
}
}
}
}
The usual code works fine. You would never try to stuff an Alert inside of a Button. You wouldn't do it here.
#main
struct MyApp: App {
#State var isOn = false
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.alert("title", isPresented: $isOn) {
Button("OK", role: .cancel) { }
}
}
}.commands {
CommandMenu("Test menu") {
Button(action: {
isOn = true
}) {
Text("Click Me")
}
}
}
}
}

Why does this navigation link not work as expected?

I tried to implement a login screen in swift 5 but the login navigation link doesn't seem to check the isActive variable from my view model. When I click it the first time it leads me to the next view although the login data is not correct. When I go back then and try to login again it checks the data.
LoginView:
import SwiftUI
struct LoginView: View {
#ObservedObject var viewModel: LoginViewModel
var username_text_field: some View {
TextField("Username", text: $viewModel.username)
}
var password_secure_field: some View {
SecureField("Password", text: $viewModel.password)
}
var login_button: some View {
NavigationLink(
destination: MenuView(),
isActive: $viewModel.is_valid
) {
Text("LOGIN").onTapGesture {
viewModel.login()
}
}
}
var body: some View {
NavigationView {
VStack {
Group {
username_text_field
password_secure_field
login_button
}
}
}
}
}
Login ViewModel:
import SwiftUI
final class LoginViewModel: ObservableObject {
#Published var username = ""
#Published var password = ""
#Published var is_valid = false
func login() {
self.is_valid = false
}
}
Is there something wrong with this approach or am I missing something else?
What Xcode version are you using? I'm on Version 12.5 (12E262) with iOS 14.5 and your code works fine. However, you might want to move the LOGIN button outside of the NavigationLink, so that it's guaranteed to only trigger based on isActive.
var login_button: some View {
Button {
viewModel.login()
} label: {
Text("LOGIN")
}
}
NavigationView {
VStack {
Group {
username_text_field
password_secure_field
login_button
NavigationLink(
destination: Text("Menu View"),
isActive: $viewModel.is_valid
) { EmptyView() } /// pass in `EmptyView`
}
}
}
Old answer:
Change login() to this:
func login() {
self.is_valid = true /// true, not false!
}
Also you should use camel case in Swift, so self.isValid instead of self.is_valid.

Picker selection within navigation link causes strange behaviour

I have a picker embedded in a form on a screen within a navigation view stack. I've re-created a simplistic version.
struct ContentView: View {
#State var showSecondView: Bool = false
var body: some View {
NavigationView {
VStack {
Button("SecondView", action: {
self.showSecondView = true
})
NavigationLink(destination: SecondContentView(), isActive: $showSecondView) {
EmptyView()
}
}
}
}
}
struct SecondContentView: View {
#State var showThirdView: Bool = false
var body: some View {
VStack {
Button("ThirdView", action: {
self.showThirdView = true
})
NavigationLink(destination: ThirdContentView(showThirdView: $showThirdView), isActive: $showThirdView) {
EmptyView()
}
}
}
}
struct ThirdContentView: View {
#Binding var showThirdView: Bool
#State var pickerSelection: String = ""
let pickerObjects = ["A", "B", "C"]
var body: some View {
VStack {
Form {
Picker(selection: $pickerSelection, label: Text("Abort Reason")
) {
ForEach(0 ..< pickerObjects.count) { i in
Text("\(self.pickerObjects[i])").tag(self.pickerObjects[i])
}
}
}
Button("Done", action: {
self.showThirdView.toggle()
})
}
}
}
In the example above when I set a value and press done it navigates back to the third screen (with the picker) but without a value selected. In my full app pressing done dismisses the third screen but then when I press back on the second screen it briefly shows the third screen for a second before dismissing it.
If I present the third view outside of a navigation link (if showThirdView == true) then no navigation errors. The setting of a value in the picker seems to add another instance of the third view to the NavigationView stack rather than going back. I like the navigation link style as the back button is consistent for the user. Is there any way to get the picker to work within a navigation link?
Here is fixed parts that works - replaced Binding, which becomes lost, with presentation mode. Tested with Xcode 12 / iOS 14.
struct SecondContentView: View {
#State var showThirdView: Bool = false
var body: some View {
VStack {
Button("ThirdView", action: {
self.showThirdView = true
})
NavigationLink(destination: ThirdContentView(), isActive: $showThirdView) {
EmptyView()
}
}
}
}
struct ThirdContentView: View {
#Environment(\.presentationMode) var mode
#State var pickerSelection: String = ""
let pickerObjects = ["A", "B", "C"]
var body: some View {
VStack {
Form {
Picker(selection: $pickerSelection, label: Text("Abort Reason")
) {
ForEach(0 ..< pickerObjects.count) { i in
Text("\(self.pickerObjects[i])").tag(self.pickerObjects[i])
}
}
}
Button("Done", action: {
self.mode.wrappedValue.dismiss()
})
}
}
}

SwiftUI - Form with error message on button press and navigation

I have the following scenario. I have a text field and a button, what I would need is to show an error message in case the field is empty and if not, navigate the user to the next screen.
I have tried showing the error message conditionally by using the field value and checking if it is empty on button press, but then, I don't know how to navigate to the next screen.
struct SomeView: View {
#State var fieldValue = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
if showErrorMessage {
Text("Error, please enter value")
}
Button(action: {
if self.fieldValue.isEmpty {
self.showErrorMessage = true
} else {
self.showErrorMessage = false
//How do I put navigation here, navigation link does not work, if I tap, nothing happens
}
}) {
Text("Next")
}
}
}
}
}
Using UIKit would be easy since I could use self.navigationController.pushViewController
Thanks to part of an answer here, here's some working code.
First, I moved everything into an EnvronmentObject to make things easier to pass to your second view. I also added a second toggle variable:
class Model: ObservableObject {
#Published var fieldValue = ""
#Published var showErrorMessage = false
#Published var showSecondView = false
}
Next, change two things in your ContentView. I added a hidden NavigationLink (with a isActive parameter) to actually trigger the push, along with changing your Button action to execute a local function:
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $model.fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
NavigationLink(destination: SecondView(), isActive: $model.showSecondView) {
Text("NavLink")
}.hidden()
Button(action: {
self.checkForText()
}) {
Text("Next")
}
.alert(isPresented: self.$model.showErrorMessage) {
Alert(title: Text("Error"), message: Text("Please enter some text!"), dismissButton: .default(Text("OK")))
}
}
}
}
func checkForText() {
if model.fieldValue.isEmpty {
model.showErrorMessage.toggle()
} else {
model.showSecondView.toggle()
}
}
}
Toggling showErrorMessage will show the Alert and toggling `showSecondView will take you to the next view.
Finally, the second view:
struct SecondView: View {
#EnvironmentObject var model: Model
var body: some View {
ZStack {
Rectangle().fill(Color.green)
// workaround
.navigationBarBackButtonHidden(true) // not needed, but just in case
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.showSecondView = false
})
Text(model.fieldValue)
}
}
func popSecondView() {
model.showSecondView.toggle()
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
This is where the above linked answer helped me. It appears there's a bug in navigation back that still exists in beta 6. Without this workaround (that toggles showSecondView) you will get sent back to the second view one more time.
You didn't post any details on the second view contents, so I took the liberty to add someText into the model to show you how to easily pass things into it can be using an EnvironmentObject. There is one bit of setup needed to do this in SceneDelegate:
var window: UIWindow?
var model = Model()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(model))
self.window = window
window.makeKeyAndVisible()
}
}
I noticed a slight change in this, depending on when your project was created (beta 6 declares an instance of contentView where older versions do not). Either way, declare an instance of model and then add the envoronmentObject modifier to contentView.
Another approach is to make the "Next" button conditionally a Button when the fieldValue is empty and a NavigationLink when the fieldValue is valid. The Button case will trigger your error message view and the NavigationLink will do the navigation for you. Keeping this close to your sample, the following seems to do the trick.
struct SomeView: View {
#State var fieldValue = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
if showErrorMessage {
Text("Please Enter Data")
}
if fieldValue == "" {
Button(action: {
if self.fieldValue == "" {
self.showErrorMessage = true
}
}, label: {
Text("Next")
})
} else {
// move on case
NavigationLink("Next", destination: Text("Next View"))
}
}
}
}
}
By using this code we can display the alert if the fields are empty else . it will navigate.
struct SomeView: View {
#State var userName = ""
#State var password = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("Enter Username", text: $userName).textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("Enter Your Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
if userName == "" || password == "" {
Button(action: {
if self.userName == "" || self.password == "" {
self.showErrorMessage = true
}
}, label: {
Text("Login")
})
} else {
// move case
NavigationLink("Login", destination: Text("Login successful"))
}
}.alert(isPresented: $showErrorMessage) { () -> Alert in
Alert(title: Text("Important Message"), message: Text("Please Fill all the Fields"), primaryButton: .default(Text("Ok")), secondaryButton: .destructive(Text("Cancel")))
}
}
}
}

How to transition Views programmatically using SwiftUI?

I want to show the user another view when the login is successful, otherwise stay on that view. I've done that with UIKit by performing a segue. Is there such an alternative in SwiftUI?
The NavigationButton solution does not work as I need to validate the user input before transitioning to the other view.
Button(action: {
let authService = AuthorizationService()
let result = authService.isAuthorized(username: self.username, password: self.password)
if(result == true) {
print("Login successful.")
// TODO: ADD LOGIC
*** HERE I WANT TO PERFORM THE SEGUE ***
presentation(MainView)
} else {
print("Login failed.")
}
}) {
Text("Login")
}
Xcode 11 beta 5.
NavigationDestinationLink and NavigationButton have been deprecated and replaced by NavigationLink.
Here's a full working example of programatically pushing a view to a NavigationView.
import SwiftUI
import Combine
enum MyAppPage {
case Menu
case SecondPage
}
final class MyAppEnvironmentData: ObservableObject {
#Published var currentPage : MyAppPage? = .Menu
}
struct NavigationTest: View {
var body: some View {
NavigationView {
PageOne()
}
}
}
struct PageOne: View {
#EnvironmentObject var env : MyAppEnvironmentData
var body: some View {
let navlink = NavigationLink(destination: PageTwo(),
tag: .SecondPage,
selection: $env.currentPage,
label: { EmptyView() })
return VStack {
Text("Page One").font(.largeTitle).padding()
navlink
.frame(width:0, height:0)
Button("Button") {
self.env.currentPage = .SecondPage
}
.padding()
.border(Color.primary)
}
}
}
struct PageTwo: View {
#EnvironmentObject var env : MyAppEnvironmentData
var body: some View {
VStack {
Text("Page Two").font(.largeTitle).padding()
Text("Go Back")
.padding()
.border(Color.primary)
.onTapGesture {
self.env.currentPage = .Menu
}
}.navigationBarBackButtonHidden(true)
}
}
#if DEBUG
struct NavigationTest_Previews: PreviewProvider {
static var previews: some View {
NavigationTest().environmentObject(MyAppEnvironmentData())
}
}
#endif
Note that the NavigationLink entity has to be present inside the View body.
If you have a button that triggers the link, you'll use the label of the NavigationLink.
In this case, the NavigationLink is hidden by setting its frame to 0,0, which is kind of a hack but I'm not aware of a better method at this point. .hidden() doesn't have the same effect.
You could do it like bellow, based on this response (it's packed like a Playground for easy testing:
import SwiftUI
import Combine
import PlaygroundSupport
struct ContentView: View {
var body: some View {
NavigationView {
MainView().navigationBarTitle(Text("Main View"))
}
}
}
struct MainView: View {
let afterLoginView = DynamicNavigationDestinationLink(id: \String.self) { message in
AfterLoginView(msg: message)
}
var body: some View {
Button(action: {
print("Do the login logic here")
self.afterLoginView.presentedData?.value = "Login successful"
}) {
Text("Login")
}
}
}
struct AfterLoginView: View {
let msg: String
var body: some View {
Text(msg)
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Although this will work, I think that, from an architectural perspective, you try to push an "imperative programming" paradigm into SwiftUI's reactive logic.
I mean, I would rather implement it with the login logic wrapped into an ObjectBinding class with an exposed isLoggedin property and make the UI react to the current state (represented by isLoggedin).
Here's a very high level example :
struct MainView: View {
#ObjectBinding private var loginManager = LoginManager()
var body: some View {
if loginManager.isLoggedin {
Text("After login content")
} else {
Button(action: {
self.loginManager.login()
}) {
Text("Login")
}
}
}
}
I used a Bool state for my login transition, it seems pretty fluid.
struct ContentView: View {
#State var loggedIn = false
var body: some View {
VStack{
if self.loggedIn {
Text("LoggedIn")
Button(action: {
self.loggedIn = false
}) {
Text("Log out")
}
} else {
LoginPage(loggedIn: $loggedIn)
}
}
}
}