Hello I have made a picker in swiftUI where the user has to accept the change on the picker.
It works well, but there is one issue. If the user selects something and then declines the change, the shown value will still be what was selected though the actual value doesn't change.
If I open the picker again without selecting anything it reverts back to the right value but how to I do this without having to open the picker?
struct ContentView: View {
#State private var selectedColorIndex = "cd"
#State private var temp = ""
#State var alert = false
private var colors = ["ab", "cd", "ef", "gh", "ij", "kl", "mn", "op", "qr", "st", "uv", "wx", "yz"]
var body: some View {
HStack {
Text("Location")
Picker("Location", selection: Binding(get: {selectedColorIndex}, set: {
temp = $0
alert = true
})) {
ForEach(colors, id: \.self) {
Text($0)
}
}.pickerStyle(MenuPickerStyle())
}.alert(isPresented: $alert) {
Alert(title: Text("are you sure"), primaryButton: .default(Text("yes"), action: {
selectedColorIndex = temp
}), secondaryButton: .default(Text("no"), action: {
//change shown value back to prev selectedColorIndex
}))
}
}
}
This does not work because the PickerView does not refresh with the value it gets from the binding. Adding a print statement in the binding confirms, that it indeed delivers the correct value.
A simple solution to this would be to force the Picker to refresh itself every time the body of the ContentView gets reavaluated. We can achieve this by manually changing the id of the Picker.
var body: some View {
HStack {
Text("Location")
Picker("Location", selection: Binding(get: {selectedColorIndex}, set: {
temp = $0
alert = true
})) {
ForEach(colors, id: \.self) {
Text($0)
}
}
.id(UUID()) // add this
.pickerStyle(MenuPickerStyle())
}.alert(isPresented: $alert) {
Alert(title: Text("are you sure"), primaryButton: .default(Text("yes"), action: {
selectedColorIndex = temp
}), secondaryButton: .default(Text("no"), action: {
// No code needed here
//change shown value back to prev selectedColorIndex
}))
}
}
If you want to know more why this works you can read my answer here: more info
Related
In a macOS SwiftUI app, I have a List of items with context menus. When a menu selection is made, the app needs to act on the correct list item. (The context menu can apply to any item, not just the selected one.)
I have a solution that works fairly well, but it has a strange bug. When you right click (or Command+click) on an item, the app sets a variable indicating which item was clicked, and also sets a flag. The flag triggers a sheet requesting confirmation of the action. The problem is that the first time you select a menu item, the sheet doesn’t use the saved item as it should. You can see because the item’s name is not in the “Ok to delete” prompt. If you close that first sheet and select another item, it works correctly, and it works for for every subsequent item from then on, even the first one you tried. It doesn’t matter which item you try first, or whether you select the item first, or anything.
import SwiftUI
struct ContentView: View {
#State private var actionTarget = Value(name: "")
#State private var isDeleting = false
#State private var selection = Value(name: "")
struct Value: Identifiable, Hashable {
let id = UUID()
var name: String
}
let values = [Value(name: "One"), Value(name: "Two"), Value(name: "Three")]
var body: some View {
List(values, selection: $selection) { value in
Text (value.name)
.tag(value)
.contextMenu(ContextMenu {
Button {
actionTarget = value
isDeleting = true
} label: { Text("Delete \(value.name)") }
})
}
.sheet(isPresented: $isDeleting) {
Text("Ok to delete \"\(actionTarget.name)?\"")
.frame(width: 300)
.padding()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { isDeleting = false }
}
ToolbarItem(placement: .destructiveAction) {
Button {
//TODO: Delete
isDeleting = false
} label: { Text("Delete") }
}
}
}
}
}
This is a bug in SwiftUI.
You can work around it by using a different version of the sheet modifier, the one that takes a Binding<Item?>. That also has the advantage that it leads you to a better data model. In your model as posted, you have separate isDeleting and actionTarget variables which can be out of sync. Instead, use a single optional variable holding the Value to be deleted, or nil if there is no deletion to be confirmed.
struct ContentView: View {
#State private var deleteRequest: Value? = nil
#State private var selection: Value? = nil
struct Value: Identifiable, Hashable {
let id = UUID()
var name: String
}
let values = [Value(name: "One"), Value(name: "Two"), Value(name: "Three")]
var body: some View {
List(values, selection: $selection) { value in
Text(value.name)
.tag(value)
.contextMenu(ContextMenu {
Button {
deleteRequest = value
} label: { Text("Delete \(value.name)") }
})
}
.sheet(
item: $deleteRequest,
onDismiss: { deleteRequest = nil }
) { item in
Text("Ok to delete \"\(item.name)?\"")
.frame(width: 300)
.padding()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
deleteRequest = nil
}
}
ToolbarItem(placement: .destructiveAction) {
Button {
print("TODO: delete \(item)")
deleteRequest = nil
} label: { Text("Delete") }
}
}
}
}
}
But the use of a toolbar inside the sheet doesn't look like a normal macOS confirmation sheet. Instead, you should use confirmationDialog.
struct ContentView: View {
#State private var deleteRequest: Value? = nil
#State private var selection: Value? = nil
struct Value: Identifiable, Hashable {
let id = UUID()
var name: String
}
let values = [Value(name: "One"), Value(name: "Two"), Value(name: "Three")]
var body: some View {
List(values, selection: $selection) { value in
Text(value.name)
.tag(value)
.contextMenu(ContextMenu {
Button {
deleteRequest = value
} label: { Text("Delete \(value.name)") }
})
}
.confirmationDialog(
"OK to delete \(deleteRequest?.name ?? "(nil)")?",
isPresented: .constant(deleteRequest != nil),
presenting: deleteRequest,
actions: { item in
Button("Cancel", role: .cancel) { deleteRequest = nil }
Button("Delete", role: .destructive) {
print("TODO: delete \(item)")
deleteRequest = nil
}
}
)
}
}
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
}
}
}
}
}
}
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()
})
}
}
}
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")))
}
}
}
}
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])
}
}
}