SwiftUI - Form with error message on button press and navigation - swift

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

Related

displaying alert on picker select SwiftUI

i want to display an alert box when a user selects something in a form picker the user then have to confirm their choice before the value changes.
right now my code looks like this(just from some tutorial):
NavigationView {
Form {
Section {
Picker("Strength", selection: $selectedStrength) {
ForEach(strengths, id: \.self) {
Text($0)
}
}
}
}
}
I have tried using onChange() but ideally the check should be before the value changes.
If you want to ask the user to confirm before the selection changes you would need to implement a custom binding with a second var. With this you would be able to cancel the selection if necessary.
struct Test: View{
#State private var selectedStrength: String = ""
#State private var askForStrength: String = ""
#State private var ask: Bool = false
let strengths = ["1","2","3"]
var body: some View{
NavigationView {
Form {
Section {
Picker("Strength", selection: Binding(get: {selectedStrength}, set: {
//assign the selection to the temp var
askForStrength = $0
// show the Alert
ask = true
})) {
ForEach(strengths, id: \.self) {
Text($0)
}
}
}
}
}.alert(isPresented: $ask) {
// Here ask the user if selection is correct and apply the temp var to the selection
Alert(title: Text("select?"), message: Text("Do you want to select \(askForStrength)"), primaryButton: .default(Text("select"), action: {selectedStrength = askForStrength}), secondaryButton: .cancel())
}
}
}
You can do it this way; having an alert after the value is clicked and a temp variable for storing pre-selected data. Code is below the image:
import SwiftUI
struct ContentView: View {
let animals = ["dog", "cat", "pig"]
#State var selected = ""
#State var finalResult = ""
#State var alert = false
var body: some View {
NavigationView {
Form {
Section {
Picker("Animals", selection: $selected) {
ForEach(animals, id: \.self) {
Text($0)
}
}
.onChange(of: selected) { _ in
alert.toggle()
}
Text("You have confirmed to select this: \(finalResult)")
}
.alert("Confirm selection?", isPresented: $alert) {
Button("Confirm", role: .destructive) {
finalResult = selected
}
}
}
}
}
}

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

Error message: Generic parameter "FalseContent" could not be inferred

When creating a if I'm getting this error message.
The if checks if a bool, called loggedIn, is true or false.
struct ContentView: View{
#State var loggedIn = false
#State var user: BarberUser?
#State var username: String = ""
#State var password: String = ""
var body: some View{
ZStack{
Group {
if !loggedIn {
VStack{
//a TextField() is here
//a SecureField() is here
Button(action: { self.loggedIn = true }) {
Text("Log in!")
}
}
}else{
if self.user?.type = .barber{
BarberView()
} else {
ClientView()
}
}
}
}
}
}
The line that is outputting this error is:
if !loggedIn {
what is the cause of this error? And how can I fix it?
If you need more details about the code ask me and I'll provide.
Edit: added more info to the code.
The first problem is that SwiftUI frequently show errors at the wrong places. You may try to extract your subviews and it'll make your code clearer and more comfortable to find bugs.
The second one, what I see in your code snippet: the compiler should show you:
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
if you leave just these lines of code:
struct IfElseStatementsInBody: View {
#State var loggedIn = false
var body: some View {
if !loggedIn {
VStack {
Text("Need to login")
Button(action: { self.loggedIn = true }) {
Text("Log in!")
}
}
} else {
Text("Main view")
}
}
}
The common way to avoid it is to wrap your views into AnyView. Remember, body is just a computed variable and it should know, what it need to return. The other way is to embed if...else into another view, like VStack or ZStack
struct IfElseStatementsInBody: View {
#State var loggedIn = false
var body: some View {
if !loggedIn {
return AnyView(ExtractedView(loggedIn: $loggedIn)) // how to extract subview from here
} else {
return AnyView(Text("Main view"))
}
}
}
// ... to here
struct ExtractedView: View {
#Binding var loggedIn: Bool
var body: some View {
VStack {
Text("Need to login")
Button(action: { self.loggedIn = true }) {
Text("Log in!")
}
}
}
}
// MARK: embed if...else into ZStack. in this case you'll see changing of
// "loggedIn" variable in the canvas
struct EmbedIfElseStatementsInBody: View {
#State var loggedIn = false
var body: some View {
ZStack {
if !loggedIn {
ExtractedView(loggedIn: $loggedIn)// how to extract subview from here
} else {
Text("Main view")
}
}
}
}
P.S. hope it'll help. in other way the error is in some other place, but I can't see it now because of lack of code.

SwiftUI: Support multiple modals

I'm trying to setup a view that can display multiple modals depending on which button is tapped.
When I add just one sheet, everything works:
.sheet(isPresented: $showingModal1) { ... }
But when I add another sheet, only the last one works.
.sheet(isPresented: $showingModal1) { ... }
.sheet(isPresented: $showingModal2) { ... }
UPDATE
I tried to get this working, but I'm not sure how to declare the type for modal. I'm getting an error of Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
struct ContentView: View {
#State var modal: View?
var body: some View {
VStack {
Button(action: {
self.modal = ModalContentView1()
}) {
Text("Show Modal 1")
}
Button(action: {
self.modal = ModalContentView2()
}) {
Text("Show Modal 2")
}
}.sheet(item: self.$modal, content: { modal in
return modal
})
}
}
struct ModalContentView1: View {
var body: some View {
Text("Modal 1")
}
}
struct ModalContentView2: View {
var body: some View {
Text("Modal 2")
}
}
This works:
.background(EmptyView().sheet(isPresented: $showingModal1) { ... }
.background(EmptyView().sheet(isPresented: $showingModal2) { ... }))
Notice how these are nested backgrounds. Not two backgrounds one after the other.
Thanks to DevAndArtist for finding this.
Maybe I missed the point, but you can achieve it either with a single call to .sheet(), or multiple calls.:
Multiple .sheet() approach:
import SwiftUI
struct MultipleSheets: View {
#State private var sheet1 = false
#State private var sheet2 = false
#State private var sheet3 = false
var body: some View {
VStack {
Button(action: {
self.sheet1 = true
}, label: { Text("Show Modal #1") })
.sheet(isPresented: $sheet1, content: { Sheet1() })
Button(action: {
self.sheet2 = true
}, label: { Text("Show Modal #2") })
.sheet(isPresented: $sheet2, content: { Sheet2() })
Button(action: {
self.sheet3 = true
}, label: { Text("Show Modal #3") })
.sheet(isPresented: $sheet3, content: { Sheet3() })
}
}
}
struct Sheet1: View {
var body: some View {
Text("This is Sheet #1")
}
}
struct Sheet2: View {
var body: some View {
Text("This is Sheet #2")
}
}
struct Sheet3: View {
var body: some View {
Text("This is Sheet #3")
}
}
Single .sheet() approach:
struct MultipleSheets: View {
#State private var showModal = false
#State private var modalSelection = 1
var body: some View {
VStack {
Button(action: {
self.modalSelection = 1
self.showModal = true
}, label: { Text("Show Modal #1") })
Button(action: {
self.modalSelection = 2
self.showModal = true
}, label: { Text("Show Modal #2") })
Button(action: {
self.modalSelection = 3
self.showModal = true
}, label: { Text("Show Modal #3") })
}
.sheet(isPresented: $showModal, content: {
if self.modalSelection == 1 {
Sheet1()
}
if self.modalSelection == 2 {
Sheet2()
}
if self.modalSelection == 3 {
Sheet3()
}
})
}
}
struct Sheet1: View {
var body: some View {
Text("This is Sheet #1")
}
}
struct Sheet2: View {
var body: some View {
Text("This is Sheet #2")
}
}
struct Sheet3: View {
var body: some View {
Text("This is Sheet #3")
}
}
I'm not sure whether this was always possible, but in Xcode 11.3.1 there is an overload of .sheet() for exactly this use case (https://developer.apple.com/documentation/swiftui/view/3352792-sheet). You can call it with an Identifiable item instead of a bool:
struct ModalA: View {
var body: some View {
Text("Hello, World! (A)")
}
}
struct ModalB: View {
var body: some View {
Text("Hello, World! (B)")
}
}
struct MyContentView: View {
enum Sheet: Hashable, Identifiable {
case a
case b
var id: Int {
return self.hashValue
}
}
#State var activeSheet: Sheet? = nil
var body: some View {
VStack(spacing: 42) {
Button(action: {
self.activeSheet = .a
}) {
Text("Hello, World! (A)")
}
Button(action: {
self.activeSheet = .b
}) {
Text("Hello, World! (B)")
}
}
.sheet(item: $activeSheet) { item in
if item == .a {
ModalA()
} else if item == .b {
ModalB()
}
}
}
}
I personally would mimic some NavigationLink API. Then you can create a hashable enum and decide which modal sheet you want to present.
extension View {
func sheet<Content, Tag>(
tag: Tag,
selection: Binding<Tag?>,
content: #escaping () -> Content
) -> some View where Content: View, Tag: Hashable {
let binding = Binding(
get: {
selection.wrappedValue == tag
},
set: { isPresented in
if isPresented {
selection.wrappedValue = tag
} else {
selection.wrappedValue = .none
}
}
)
return background(EmptyView().sheet(isPresented: binding, content: content))
}
}
enum ActiveSheet: Hashable {
case first
case second
}
struct First: View {
var body: some View {
Text("frist")
}
}
struct Second: View {
var body: some View {
Text("second")
}
}
struct TestView: View {
#State
private var _activeSheet: ActiveSheet?
var body: some View {
print(_activeSheet as Any)
return VStack
{
Button("first") {
self._activeSheet = .first
}
Button("second") {
self._activeSheet = .second
}
}
.sheet(tag: .first, selection: $_activeSheet) {
First()
}
.sheet(tag: .second, selection: $_activeSheet) {
Second()
}
}
}
I wrote a library off plivesey's answer that greatly simplifies the syntax:
.multiSheet {
$0.sheet(isPresented: $sheetAPresented) { Text("Sheet A") }
$0.sheet(isPresented: $sheetBPresented) { Text("Sheet B") }
$0.sheet(isPresented: $sheetCPresented) { Text("Sheet C") }
}
I solved this by creating an observable SheetContext that holds and manages the state. I then only need a single context instance and can tell it to present any view as a sheet. I prefer this to the "active view" binding approach, since you can use this context in multiple ways.
I describe it in more details in this blog post: https://danielsaidi.com/blog/2020/06/06/swiftui-sheets
I think i found THE solution. It's complicated so here is the teaser how to use it:
Button(action: {
showModal.wrappedValue = ShowModal {
AnyView( TheViewYouWantToPresent() )
}
})
Now you can define at the button level what you want to present. And the presenting view does not need to know anything. So you call this on the presenting view.
.background(EmptyView().show($showModal))
We call it on the background so the main view does not need to get updated, when $showModal changes.
Ok so what do we need to get this to work?
1: The ShowModal class:
public enum ModalType{
case sheet, fullscreen
}
public struct ShowModal: Identifiable {
public let id = ""
public let modalType: ModalType
public let content: () -> AnyView
public init (modalType: ModalType = .sheet, #ViewBuilder content: #escaping () -> AnyView){
self.modalType = modalType
self.content = content
}
}
Ignore id we just need it for Identifiable. With modalType we can present the view as sheet or fullscreen. And content is the passed view, that will be shown in the modal.
2: A ShowModal binding which stores the information for presenting views:
#State var showModal: ShowModal? = nil
And we need to add it to the environment of the view thats responsible for presentation. So we have easy access to it down the viewstack:
VStack{
InnerViewsThatWantToPresentModalViews()
}
.environment(\.showModal, $showModal)
.background(EmptyView().show($showModal))
In the last line we call .show(). Which is responsible for presentation.
Keep in mind that you have to create #State var showModal and add it to the environment again in a view thats shown modal and wants to present another modal.
4: To use .show we need to extend view:
public extension View {
func show(_ modal: Binding<ShowModal?>) -> some View {
modifier(VM_Show(modal))
}
}
And add a viewModifier that handles the information passed in $showModal
public struct VM_Show: ViewModifier {
var modal: Binding<ShowModal?>
public init(_ modal: Binding<ShowModal?>) {
self.modal = modal
}
public func body(content: Content) -> some View {
guard let modalType = modal.wrappedValue?.modalType else{ return AnyView(content) }
switch modalType {
case .sheet:
return AnyView(
content.sheet(item: modal){ modal in
modal.content()
}
)
case .fullscreen:
return AnyView(
content.fullScreenCover(item: modal) { modal in
modal.content()
}
)
}
}
}
4: Last we need to set showModal in views that want to present a modal:
Get the variable with: #Environment(\.showModal) var showModal. And set it like this:
Button(action: {
showModal.wrappedValue = ShowModal(modalType: .fullscreen) {
AnyView( TheViewYouWantToPresent() )
}
})
In the view that defined $showModal you set it without wrappedValue: $showModal = ShowModal{...}
As an alternative, simply putting a clear pixel somewhere in your layout might work for you:
Color.clear.frame(width: 1, height: 1, alignment: .center).sheet(isPresented: $showMySheet, content: {
MySheetView();
})
Add as many pixels as necessary.

Correct way to present an ActionSheet with SwiftUI

I want to show ActionSheet (or any other modal, but Alert) on some event like button tap.
I found the way of doing it using the state variable. It seems a little bit strange for me to display it that way because I have to reset variable when ActionSheet closes manually.
Is there a better way to do it?
Why there is a separate method for presenting Alert that allows you to bind its visibility to a state variable? What's the difference with my approach?
struct Sketch : View {
#State var showActionSheet = false
var body: some View {
ZStack {
Button(action: { showActionSheet = true }) { Text("Show") }
}
.presentation(showActionSheet ?
ActionSheet(
title: Text("Action"),
buttons: [
ActionSheet.Button.cancel() {
self. showActionSheet = false
}
])
: nil)
}
}
Enforcing the preference for the state variable approach, Apple has adopted for the alert and action sheet APIs. For the benefit of others who find this question, here are updated examples of all 3 types based on Xcode 11 beta 7, iOS 13.
#State var showAlert = false
#State var showActionSheet = false
#State var showAddModal = false
var body: some View {
VStack {
// ALERT
Button(action: { self.showAlert = true }) {
Text("Show Alert")
}
.alert(isPresented: $showAlert) {
// Alert(...)
// showAlert set to false through the binding
}
// ACTION SHEET
Button(action: { self.showActionSheet = true }) {
Text("Show Action Sheet")
}
.actionSheet(isPresented: $showActionSheet) {
// ActionSheet(...)
// showActionSheet set to false through the binding
}
// FULL-SCREEN VIEW
Button(action: { self.showAddModal = true }) {
Text("Show Modal")
}
.sheet(isPresented: $showAddModal, onDismiss: {} ) {
// INSERT a call to the new view, and in it set showAddModal = false to close
// e.g. AddItem(isPresented: self.$showAddModal)
}
}
For the modal part of your question, you could use a PresentationButton:
struct ContentView : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: DetailView())
}
}
Source
struct Sketch : View {
#State var showActionSheet = false
var body: some View {
ZStack {
Button(action: { self.showActionSheet.toggle() }) { Text("Show") }
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Test"))
}
}
}
}
This would work, a #State is a property wrapper and your action sheet will keep an eye on it, whenever it turns to true, the action sheet will be shown
Below is the best way to show multiple action sheets according to requirements.
struct ActionSheetBootCamp: View {
#State private var isShowingActionSheet = false
#State private var sheetType: SheetTypes = .isOtherPost
enum SheetTypes {
case isMyPost
case isOtherPost
}
var body: some View {
HStack {
Button("Other Post") {
sheetType = .isOtherPost
isShowingActionSheet.toggle()
}
Button("My post") {
sheetType = .isMyPost
isShowingActionSheet.toggle()
}
}.actionSheet(isPresented: $isShowingActionSheet, content: getActionSheet)
}
func getActionSheet() -> ActionSheet {
let btn_share: ActionSheet.Button = .default(Text("Share")) {
//Implementation
}
let btn_report: ActionSheet.Button = .destructive(Text("Report")) {
//Implementation
}
let btn_edit: ActionSheet.Button = .default(Text("Edit")) {
//Implementation
}
let btn_cancel: ActionSheet.Button = .cancel()
switch(sheetType) {
case .isMyPost:
return ActionSheet(title: Text("This is the action sheet title"), message: nil, buttons: [btn_share,btn_edit, btn_cancel])
case .isOtherPost:
return ActionSheet(title: Text("This is the action sheet title"), message: nil, buttons: [btn_share,btn_report, btn_cancel])
}
}
}