Correct way to present an ActionSheet with SwiftUI - swift

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

Related

How to navigate to different views on a button click using navigation link?

I need to navigate to different views based on a variable value using navigation link. Could you please help on this
Use NavigationLink(destination: , label: ) as follows:
struct TestView: View {
var viewName = ""
var body: some View {
VStack {
NavigationLink {
if viewName == "firstView" {
FirstView()
} else if name == "secondView" {
SecondView()
} else if name == "thirdView" {
ThirdView()
}
} label: {
Text("Go to specified view")
}
}
}
}
I have done it using NavigationStack.
Example:- Initially shouldShowAgeView is false. When "Show DOB" button is clicked, we will change shouldShowAgeView to true and when it is true AgeView will be shown.
struct DetailView: View {
#State private var shouldShowAgeView = false
#State private var shouldShowDOBView = false
#State private var shouldShowWeightView = false
var body: some View {
NavigationStack {
VStack {
Button("Show DOB", action: showDob) // showDob function will be called on when tapped on the button
Button("Show Age", action: showAge) // showAge function will be called on when tapped on the button
Button("Show Weight", action: showWeight) // showRollNumber function will be called on when tapped on the button
}
.navigationDestination(isPresented: $shouldShowAgeView) { // if is true then AgeView is shown
AgeView()
}
.navigationDestination(isPresented: $shouldShowDOBView) { // if is true then DOBView is shown
DOBView()
}
.navigationDestination(isPresented: $shouldShowWeightView) { // if is true then WeightView is shown
WeightView()
}
}
.onAppear{
shouldShowAgeView = false
shouldShowDOBView = false
shouldShowWeightView = false
}
}
func showAge() {
shouldShowAgeView = true
}
func showDob() {
shouldShowDOBView = true
}
func showWeight() {
shouldShowWeightView = true
}
}

Unable to use 2 popovers in a single view

I created a new Single View App in Xcode 11.4.1 that uses Swift and SwiftUI. The project has 2 buttons with the options to show either popover 1 or popover 2. I don't receive any errors but only popover 2 works. My code is below.
I tried rearranging the position of the popover code but it did not make a difference. If the popover 1 code appears after the popover 2 code then only popover 1 works (instead of only having popover 2 work).
import SwiftUI
struct ContentView: View {
#State var popover1IsVisible = false
#State var popover2IsVisible = false
var body: some View {
VStack {
Button(action: {
self.popover1IsVisible = true
}) {
Text("Show Popover 1")
}
Button(action: {
self.popover2IsVisible = true
}) {
Text("Show Popover 2")
}
}
.popover(isPresented: $popover1IsVisible) {
VStack {
Text("Popover1")
Button(action: {
self.popover1IsVisible = false
}) {
Text("OK")
}
}
}
.popover(isPresented: $popover2IsVisible) {
VStack {
Text("Popover 2")
Button(action: {
self.popover2IsVisible = false
}) {
Text("OK")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can not add .popover twice to the view. Try to use it once and build the popover view dynamically depending on which button was clicked.
struct ContentView: View {
#State var showPopover = false
#State var popover1IsVisible : Bool = false
#State var popover2IsVisible : Bool = false
var body: some View {
VStack {
Button(action: {
self.showPopover = true
self.popover1IsVisible = true
}) {
Text("Show Popover 1")
}
Button(action: {
self.showPopover = true
self.popover2IsVisible = true
}) {
Text("Show Popover 2")
}
}
.popover(isPresented: $showPopover) {
VStack {
if (self.popover1IsVisible)
{
//Show first view
Text("Popover1")
Button(action: {
self.showPopover = false
self.popover1IsVisible = false
}) {
Text("OK")
}
}
else
{
//Show secondview
Text("Popover 2")
Button(action: {
self.showPopover = false
self.popover2IsVisible = false
}) {
Text("OK")
}
}
}
}
}
}
You can't attach two popovers to one view (VStack). Attach each popover to different views. You can attach .popover to Button.

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

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.