Please help me get rid of this problem:
Steps to reproduce my problem:
Tap "Edit My Name" Button
Inside .sheet, tap on the TextField and then with keyboard still shown, scroll all the way down
Tap on the Button "Delete Name"
Here is the problem:
confirmationDialog appears only for one second, and then
disappears, not giving the user any chance (or less than one second chance)
to tap one of the confirmationDialog's Buttons!
Here's my code:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var myName = "Joe"
#State private var isEditingName = false
var body: some View {
Text("My name is: \(myName)")
Button("Edit My Name") {
isEditingName = true
}
.padding()
.sheet(isPresented: $isEditingName) {
EditView(name: $myName)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
EditView.swift
import SwiftUI
struct EditView: View {
#Binding var name: String
#State private var isShowingConfirmationDialog = false
var body: some View {
Form {
Section {
TextField("Name", text: $name)
}
Section {
VStack {
ForEach(0..<50, id: \.self) { number in
Text("\(number)")
}
}
}
Section {
deleteNameWithConfirmationDialog
}
}
}
private var deleteNameWithConfirmationDialog: some View {
Button("Delete Name", role: .destructive) {
isShowingConfirmationDialog = true
}
.confirmationDialog("Are you sure you want to delete name?", isPresented: $isShowingConfirmationDialog) {
Button("Delete Name", role: .destructive) {
name = ""
}
Button("Cancel", role: .cancel) { }
} message: {
Text("Are you sure you want to delte name?")
}
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(name: .constant(String("Joe")))
}
}
It works if you move the .confirmationDialogue out of the Form:
struct EditView: View {
#Binding var name: String
#State private var isShowingConfirmationDialog = false
var body: some View {
Form {
Section {
TextField("Name", text: $name)
}
Section {
VStack {
ForEach(0..<50, id: \.self) { number in
Text("\(number)")
}
}
}
Section {
Button("Delete Name", role: .destructive) {
isShowingConfirmationDialog = true
}
}
}
.confirmationDialog("Are you sure you want to delete name?", isPresented: $isShowingConfirmationDialog) {
Button("Delete Name", role: .destructive) {
name = ""
}
Button("Cancel", role: .cancel) { }
} message: {
Text("Are you sure you want to delete name?")
}
}
}
If you want to keep .confirmationDialog inside of the Form, or if you are using .confirmationDialog to manage deleting items within a List, you can also avoid the immediate dismissal by excluding the .destructive role from the deleteButton in .swipeActions.
var body: some View {
List { // this could also be a Form
ForEach(listItems) { item in
ItemRow(item: item) // confirmationDialog is in ItemRow
}.swipeActions {
Button(action: { /* deleteMethodHere */ }) {
Image(systemName: "trash")
}.tint(.red) // to keep the swipe button red
}
}
}
Related
I have, what is probably, a beginner question here. I'm hoping there is something simple I'm missing or I have done wrong.
I essentially have a view which holds a struct containing an array of id strings. I then have a #FirestoreQuery which accesses a collection which holds objects with these id's. My view then displays a list with two sections. One for the id's in the original struct, and one for the remaining ones in the collection which don't appear in the array.
Each listitem is a separate view which displays the details of that item and also includes a button. When this button is pressed it adds/removes that object from the parent list and the view should update to show that object in the opposite section of the list from before.
My issue is that this works fine in the 'preview' in xcode when I look at this view on it's own. However if I run the app in the simulator, or even preview a parent view and navigate to this one, the refreshing of the view doesn't seem to work. I can press the buttons, and nothing happens. If i leave the view and come back, everything appears where it should.
I'll include all the files below. Is there something I'm missing here?
Thanks
Main view displaying the list with two sections
import SwiftUI
import FirebaseFirestoreSwift
struct SessionInvitesView: View {
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#Binding var sessionViewModel : TrainingSessionViewModel
#State private var searchText: String = ""
#State var refresh : Bool = false
var enrolledClients : [Client] {
return clients.filter { sessionViewModel.session.invites.contains($0.id!) }
}
var availableClients : [Client] {
return clients.filter { !sessionViewModel.session.invites.contains($0.id!) }
}
var searchFilteredClients : [Client] {
if searchText.isEmpty {
return availableClients
} else {
return availableClients.filter {
$0.dogName.localizedCaseInsensitiveContains(searchText) ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
$0.dogBreed.localizedCaseInsensitiveContains(searchText) }
}
}
var backButton: some View {
Button(action: { self.onCancel() }) {
Text("Back")
}
}
var body: some View {
NavigationView {
List {
Section(header: Text("Enrolled")) {
ForEach(enrolledClients) { client in
SessionInviteListItem(client: client, isEnrolled: true, onTap: removeClient)
}
}
Section(header: Text("Others")) {
ForEach(searchFilteredClients) { client in
SessionInviteListItem(client: client, isEnrolled: false, onTap: addClient)
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $searchText)
.navigationTitle("Invites")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: backButton)
}
}
func removeClient(clientId: String) {
self.sessionViewModel.session.invites.removeAll(where: { $0 == clientId })
refresh.toggle()
}
func addClient(clientId: String) {
self.sessionViewModel.session.invites.append(clientId)
refresh.toggle()
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
}
struct SessionInvitesView_Previews: PreviewProvider {
#State static var model = TrainingSessionViewModel()
static var previews: some View {
SessionInvitesView(sessionViewModel: $model)
}
}
List item view
import SwiftUI
struct SessionInviteListItem: View {
var client : Client
#State var isEnrolled : Bool
var onTap : (String) -> ()
var body: some View {
HStack {
VStack(alignment: .leading) {
HStack {
Text(client.dogName.uppercased())
.bold()
Text("(\(client.dogBreed))")
}
Text(client.name)
.font(.subheadline)
}
Spacer()
Button(action: { onTap(client.id!) }) {
Image(systemName: self.isEnrolled ? "xmark.circle.fill" : "plus.circle.fill")
}
.buttonStyle(.borderless)
.foregroundColor(self.isEnrolled ? .red : .green)
}
}
}
struct SessionInviteListItem_Previews: PreviewProvider {
static func doNothing(_ : String) {}
static var previews: some View {
SessionInviteListItem(client: buildSampleClient(), isEnrolled: false, onTap: doNothing)
}
}
Higher level view used to navigate to this list view
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionEditView: View {
// MARK: - Member Variables
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#StateObject var sheetManager = SheetManager()
var mode: Mode = .new
var dateManager = DateManager()
#State var viewModel = TrainingSessionViewModel()
#State var sessionDate = Date.now
#State var startTime = Date.now
#State var endTime = Date.now.addingTimeInterval(3600)
var completionHandler: ((Result<Action, Error>) -> Void)?
// MARK: - Local Views
var cancelButton: some View {
Button(action: { self.onCancel() }) {
Text("Cancel")
}
}
var saveButton: some View {
Button(action: { self.onSave() }) {
Text("Save")
}
}
var addInviteButton : some View {
Button(action: { sheetManager.showInvitesSheet.toggle() }) {
HStack {
Text("Add")
Image(systemName: "plus")
}
}
}
// MARK: - Main View
var body: some View {
NavigationView {
List {
Section(header: Text("Details")) {
TextField("Session Name", text: $viewModel.session.title)
TextField("Location", text: $viewModel.session.location)
}
Section {
DatePicker(selection: $sessionDate, displayedComponents: .date) {
Text("Date")
}
.onChange(of: sessionDate, perform: { _ in
viewModel.session.date = dateManager.dateToStr(date: sessionDate)
})
DatePicker(selection: $startTime, displayedComponents: .hourAndMinute) {
Text("Start Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: startTime, perform: { _ in
viewModel.session.startTime = dateManager.timeToStr(date: startTime)
})
DatePicker(selection: $endTime, displayedComponents: .hourAndMinute) {
Text("End Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: endTime, perform: { _ in
viewModel.session.endTime = dateManager.timeToStr(date: endTime)
})
}
Section {
HStack {
Text("Clients")
Spacer()
Button(action: { self.sheetManager.showInvitesSheet.toggle() }) {
Text("Edit").foregroundColor(.blue)
}
}
ForEach(viewModel.session.invites, id: \.self) { clientID in
self.createClientListElement(id: clientID)
}
.onDelete(perform: deleteInvite)
}
Section(header: Text("Notes")) {
TextField("Add notes here...", text: $viewModel.session.notes)
}
if mode == .edit {
Section {
HStack {
Spacer()
Button("Delete Session") {
sheetManager.showActionSheet.toggle()
}
.foregroundColor(.red)
Spacer()
}
}
}
}
.navigationTitle(mode == .new ? "New Training Session" : "Edit Training Session")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading: cancelButton,
trailing: saveButton)
.actionSheet(isPresented: $sheetManager.showActionSheet) {
ActionSheet(title: Text("Are you sure?"),
buttons: [
.destructive(Text("Delete Session"), action: { self.onDelete() }),
.cancel()
])
}
.sheet(isPresented: $sheetManager.showInvitesSheet) {
SessionInvitesView(sessionViewModel: $viewModel)
}
}
}
func createClientListElement(id: String) -> some View {
let client = clients.first(where: { $0.id == id })
if let client = client {
return AnyView(ClientListItem(client: client))
}
else {
return AnyView(Text("Invalid Client ID: \(id)"))
}
}
func deleteInvite(indexSet: IndexSet) {
viewModel.session.invites.remove(atOffsets: indexSet)
}
// MARK: - Local Event Handlers
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
func onSave() {
self.viewModel.onDone()
self.dismiss()
}
func onDelete() {
self.viewModel.onDelete()
self.dismiss()
self.completionHandler?(.success(.delete))
}
// MARK: - Sheet Management
class SheetManager : ObservableObject {
#Published var showActionSheet = false
#Published var showInvitesSheet = false
}
}
struct TrainingSessionEditView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionEditView(viewModel: TrainingSessionViewModel(session: buildSampleTrainingSession()))
}
}
I'm happy to include any of the other files if you think it would help. Thanks in advance!
I´ve got a list of Fetched Core Data Items, displayed as a NavigationLink inside a ForEach Loop.
Each of those Elements can be deleted by Swipe or Context Menu.
However when I add an additional confirmationDialog, and move the actual delete action into that one, the wrong item gets deleted (until the actual selected Item is the last one).
Without the confirmationDialog, and the delete Action inside the Button, it works fine.
Does anyone have any idea why?
Thank you!
import Foundation
import SwiftUI
struct IngredientsList: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: []) private var ingredients: FetchedResults<Ingredient>
#State private var DeleteDialogue = false
var body: some View {
VStack{
List{
ForEach(ingredients){ingredients in
NavigationLink{
RecipeIngredientsDetailed(ingredients: ingredients, editMode: true, createmode: true)
} label: {
Text(ingredients.ingredientname ?? "")
}
.swipeActions(){
Button(role: .destructive){
DeleteDialogue = true
} label:{
Text("Delete")
}
}
.contextMenu(){
Button(role: .destructive){
DeleteDialogue = true
} label:{
Text("Delete")
}
}
.confirmationDialog("Are you sure?", isPresented: $DeleteDialogue){
Button("Delete Ingredient"){
viewContext.delete(ingredients)
do{
try viewContext.save()
} catch{
}
}
} message: {
Text("This will remove the Ingredient from all Recipes!")
}
}
}
}
}
}
Its because you are using the same boolean for every item. Try making custom View struct for each row, that has its own boolean, e.g.
struct IngredientsList: View {
#FetchRequest(sortDescriptors: []) private var ingredients: FetchedResults<Ingredient>
var body: some View {
List{
ForEach(ingredients){ ingredient in
IngredientRow(ingredient: ingredient)
}
}
}
}
struct IngredientRow: View {
#Environment(\.managedObjectContext) private var viewContext
#State var confirm = false
#ObservedObject var ingredient: Ingredient
var body: some View {
NavigationLink{
IngredientDetail(ingredient: ingredient)
} label: {
Text(ingredients.ingredientname ?? "")
}
.swipeActions {
Button(role: .destructive){
confirm = true
} label: {
Text("Delete")
}
}
.contextMenu {
Button(role: .destructive){
confirm = true
} label:{
Text("Delete")
}
}
.confirmationDialog("Are you sure?", isPresented: $confirm){
Button("Delete Ingredient"){
viewContext.delete(ingredient)
do {
try viewContext.save()
} catch {
}
}
} message: {
Text("This will remove the Ingredient from all Recipes!")
}
}
}
And btw, without a valid sortDescriptor the list might not behave correctly.
I am curious why this .fullScreenCover display of a view does not update properly with a passed-in parameter unless the parameter is using the #Binding property wrapper. Is this a bug or intended behavior? Is this the fact that the view shown by the fullScreenCover is not lazily generated?
import SwiftUI
struct ContentView: View {
#State private var showFullScreen = false
#State private var message = "Initial Message"
var body: some View {
VStack {
Button {
self.message = "new message"
showFullScreen = true
} label: {
Text("Show Full Screen")
}
}.fullScreenCover(isPresented: $showFullScreen) {
TestView(text: message)
}
}
}
struct TestView: View {
var text: String
var body: some View {
Text(text)
}
}
There is a different fullScreenCover for passing in dynamic data, e.g.
import SwiftUI
struct CoverData: Identifiable {
var id: String {
return message
}
let message: String
}
struct FullScreenCoverTestView: View {
#State private var coverData: CoverData?
var body: some View {
VStack {
Button {
coverData = CoverData(message: "new message")
} label: {
Text("Show Full Screen")
}
}
.fullScreenCover(item: $coverData, onDismiss: didDismiss) { item in
TestView(text: item.message)
.onTapGesture {
coverData = nil
}
}
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct TestView: View {
let text: String
var body: some View {
Text(text)
}
}
More info and an example in the docs:
https://developer.apple.com/documentation/SwiftUI/AnyView/fullScreenCover(item:onDismiss:content:)
I have a List with multiple Section and each Section has different type of data. For each section I want clicking on an item to present a popover.
Problem is that if I attach the .popover to the Section or ForEach then the .popover seems to be applied to every entry in the list. So the popover gets created for each item even when just one is clicked.
Example code is below. I cannot attach the .popover to the List because, in my case, there are 2 different styles of .popover and each view can only have a single .popover attached to it.
struct Item: Identifiable {
var id = UUID()
var title: String
}
var items: [Item] = [
Item(title: "Item 1"),
Item(title: "Item 2"),
Item(title: "Item 3"),
]
struct PopoverView: View {
#State var item: Item
var body: some View {
print("new PopoverView")
return Text("View for \(item.title)")
}
}
struct ContentView: View {
#State var currentItem: Item?
var body: some View {
List {
Section(header: Text("Items")) {
ForEach(items) { item in
Button(action: { currentItem = item }) {
Text("\(item.title)")
}
}
}
}
}
}
The current best solution I have come up with is to attach the popover to each Button and then only allow one popover based on currentItem,
Button(action: { currentItem = item }) {
Text("\(item.title)")
}
.popover(isPresented: .init(get: { currentItem == item },
set: { $0 ? (currentItem = item) : (currentItem = nil) })) {
PopoverView(item: item)
}
Any better way to do this?
Bonus points to solve this: When I used my hack, the drag down motion seems to glitch and the view appears from the top again. Not sure what the deal with that is.
You can always create a separate view for your item.
struct MyGreatItemView: View {
#State var isPresented = false
var item: Item
var body: some View {
Button(action: { isPresented = true }) {
Text("\(item.title)")
}
.popover(isPresented: $isPresented) {
PopoverView(item: item)
}
}
}
And implement it to ContentView:
struct ContentView: View {
var body: some View {
List {
Section(header: Text("Items")) {
ForEach(items) { item in
MyGreatItemView(item: item)
}
}
}
}
}
Trying to reach component like sheet or popover in ForEach causes problems.
I've also faced the glitch you mentioned, but below (with sheet) works as expected;
List {
Section(header: Text("Items")) {
ForEach(items) { item in
Button(action: { currentItem = item }) {
Text("\(item.title)")
}
}
}
}
.sheet(item: $currentItem, content: PopoverView.init)
Here's a late suggestion, I used a ViewModifier to hold the show Popover state on each view, the modifier also builds the Popover menu and also handles the presented sheet initiated from the popover menu. (Here's some code...)
struct Item: Identifiable {
var id = UUID()
var title: String
}
var items: [Item] = [
Item(title: "Item 1"),
Item(title: "Item 2"),
Item(title: "Item 3"),
]
struct ContentView: View {
var body: some View {
List {
Section(header: Text("Items")) {
ForEach(items) { item in
Text("\(item.title)").popoverWithSheet(item: item)
}
}
}
}
}
struct SheetFromPopover: View {
#State var item: Item
var body: some View {
print("new Sheet from Popover")
return Text("Sheet for \(item.title)")
}
}
struct PopoverModifierForView : ViewModifier {
#State var showSheet : Bool = false
#State var showPopover : Bool = false
var item : Item
var tap: some Gesture {
TapGesture(count: 1)
.onEnded { _ in self.showPopover = true }
}
func body(content: Content) -> some View {
content
.popover(isPresented: $showPopover,
attachmentAnchor: .point(.bottom),
arrowEdge: .bottom) {
self.createPopover()
}
.sheet(isPresented: self.$showSheet) {
SheetFromPopover(item: item)
}
.gesture(tap)
}
func createPopover() -> some View {
VStack {
Button(action: {
self.showPopover = false
self.showSheet = true
}) {
Text("Show Sheet...")
}.padding()
Button(action: {
print("Something Else..")
}) {
Text("Something Else")
}.padding()
}
}
}
extension View {
func popoverWithSheet(item: Item) -> some View {
modifier(PopoverModifierForView(item: item))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am learning SwiftUI (New framework provided by Apple with iOS 13 and Xcode 11 : SwiftUI by Apple).
I want to add Button and TextField in ListView with action. I want one textfield in that user can add any one number from 1 to 10 and then hit SEND button. Anyone have any idea how to add button in it and also how can we handle touch event of Button with SwiftUI ?
Any help would be appreciate.
Here is a simple view what contains a textfield and a button in a horizontal stack.
To handle the user interaction with in your Button, just overwrite the action closure.
import SwiftUI
struct ButtonAndTextFieldView : View {
#State var text: String = ""
var body: some View {
HStack {
TextField($text,
placeholder: Text("type something here..."))
Button(action: {
// Closure will be called once user taps your button
print(self.$text)
}) {
Text("SEND")
}
}
}
}
#if DEBUG
struct ButtonWithTextFieldView_Previews : PreviewProvider {
static var previews: some View {
ButtonWithTextFieldView()
}
}
#endif
For the Login page design you can use this code section. With textFieldStyle border textfield and content type set.
struct ButtonAndTextFieldView : View {
#State var email: String = ""
#State var password: String = ""
var body: some View {
VStack {
TextField($email,
placeholder: Text("email"))
.textFieldStyle(.roundedBorder)
.textContentType(.emailAddress)
TextField($password,
placeholder: Text("password"))
.textFieldStyle(.roundedBorder)
.textContentType(.password)
Button(action: {
//Get Email and Password
print(self.$email)
print(self.$password)
}) {
Text("Send")
}
}
}
You can add button like that
Button(action: {}) {
Text("Increment Total")
}
And text field.
#State var bindingString: Binding<String> = .constant("")
TextField(bindingString,
placeholder: Text("Hello"),
onEditingChanged: { editing in
print(editing)
}).padding(.all, 40)
You can write a custom TextField which will return you the event in the closure once the user taps on the button. This Custom textfield would contain a HStack with a textfield and a button. Like this.
struct CustomTextField : View {
#Binding var text: String
var editingChanged: (Bool)->() = { _ in }
var commit: ()->() = { }
var action : () -> Void
var buttonTitle : String
var placeholder: String
var isSecuredField = false
var body : some View {
HStack {
if isSecuredField {
SecureField(placeholder, text: $text, onCommit: commit)
} else {
TextField(placeholder, text: $text, onEditingChanged: editingChanged, onCommit: commit)
}
Button(action: action) {
Text(buttonTitle)
}
}
}
}
And to you can use this custom TextField like this. I have used an example from the above-listed answers to make it more clear.
struct ListView: View {
#State var text: String = ""
var body: some View {
List {
ForEach (1..<2) {_ in
Section {
CustomTextField(
text: self.$text,
action: {
print("number is .....\(self.text)")
},
buttonTitle: "Submit",
placeholder: "enter your number")
}
}
}
}
}
ListView with textfield and button. You will need an identifier for each row in case you want to have multiple rows in the List.
struct ListView: View {
#State var text: String = ""
var body: some View {
List {
ForEach (1..<2) {_ in
Section {
HStack(alignment: .center) {
TextField(self.$text, placeholder: Text("type something here...") ).background(Color.red)
Button(action: {
print(self.$text.value)
} ) {
Text("Send")
}
}
}
}
}
}
}