swipeActions in a list: button can not be pressed - list

I'm trying to add a simple swipe action to my list. But for some reason the button can not be pressed. When I perform a full swipe it works though.
This is my code:
var listView: some View {
List {
ForEach(Array(debits.enumerated()), id: \.element) { index, debit in
HStack {
Text(debit.name)
.swipeActions(allowsFullSwipe: true) {
Button(action: {
print("Hi \(index)")
}, label: {
Image(systemName: "slider.horizontal.3")
})
}
Spacer()
Text(String(debit.value))
.frame(width: 70, alignment: .trailing)
Text("€")
Divider().padding(.leading, 5)
Toggle("", isOn: $debits[index].toggle)
.onChange(of: debit.toggle) { newValue in
calculateAvailable()
}
.frame(width: 50)
}
}
}.listStyle(.plain).lineLimit(1)
}
Like I said, a press on the button does not print anything but the full swipe does.

Move the .swipeActions modifier to the HStack containing the whole row:
List {
ForEach(Array(debits.enumerated()), id: \.element) { index, debit in
HStack {
Text(debit.name)
Spacer()
Text(String(debit.value))
.frame(width: 70, alignment: .trailing)
Text("€")
Divider().padding(.leading, 5)
Toggle("", isOn: $debits[index].toggle)
.onChange(of: debit.toggle) { newValue in
calculateAvailable()
}
.frame(width: 50)
}
.swipeActions(allowsFullSwipe: true) {
Button(action: {
print("Hi \(index)")
}, label: {
Image(systemName: "slider.horizontal.3")
})
}
}
}
Also, I would recommend adding a stable id to your Debit structure. Something like this:
struct Debit: Hashable, Identifiable {
var name: String
var value: Double
var toggle: Bool
let id = UUID()
}
and use that as your id:
ForEach(Array(debits.enumerated()), id: \.element.id) { index, debit in
If you weren’t enumerating to get the index, then you’d simply be able to iterate over debits since the items are Identifiable:
ForEach($debits) { $debit in

This is working fine, the HStack is your problem. Try to set the swipaction on the HStack
List {
ForEach(YourList, id: \.self) { debit in
Text(debit)
.swipeActions(allowsFullSwipe: true) {
Button(action: {
print("Hi \(index)")
}, label: {
Text("test")
})
}
}
}
enter code here

I just found the answer. I used this code to dismiss the keyboard when tapping somewhere. This prevented the button from being pressed.
var body: some View {
VStack {
my code
}
.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}

I also had this exact same problem and it turns out a containing view had an .onTapGesture handler which was capturing the presses on the button effectively making it only work after the full swipe, but not when tapped.

Related

Problem implementing scrollTo in SwiftUI using Slider and Button

I'm trying to reposition a list using scrollTo with a Slider and Button and have 2 problems. The list has Sections with id's and nested Texts below, so the scrolling (by Slider or Button) is to the Section heads only.
Here are the problems:
Slider works fine except that the animation briefly shows the list at the top (you can see the navigationTitle go to full size) before it scrolls to the desired position correctly.
The Button moves the Slider but appears to not find the id's in the Section, so it doesn't reposition to the right Section.
Anyone have any ideas?
struct Floors: Identifiable {
let id: String
let name: String
}
struct UnitLine: Identifiable {
let id=UUID()
let name: String
}
struct UnitKeyboardView: View {
#Environment(\.presentationMode) var presentationMode
#State private var sliderValue = 8.0
var body: some View {
let floors=[Floors(id:"8",name:"8"),Floors(id:"9",name:"9"),Floors(id:"10",name:"10"),Floors(id:"11",name:"11"),Floors(id:"12",name:"12"),Floors(id:"13",name:"13"),Floors(id:"14",name:"14"),Floors(id:"15",name:"15"),Floors(id:"16",name:"16"),Floors(id:"17",name:"17"),Floors(id:"18",name:"18"),Floors(id:"19",name:"19"),Floors(id:"20",name:"20"),Floors(id:"21",name:"21"),Floors(id:"22",name:"22"),Floors(id:"23",name:"23"),Floors(id:"24",name:"24"),Floors(id:"25",name:"25"),Floors(id:"26",name:"26"),Floors(id:"27",name:"27"),Floors(id:"28",name:"28"),Floors(id:"29",name:"29")]
let unitlines=[UnitLine(name:"01"),UnitLine(name:"02"),UnitLine(name:"03"),UnitLine(name:"04"),UnitLine(name:"05"),UnitLine(name:"06"),UnitLine(name:"07"),UnitLine(name:"08")]
NavigationView {
ScrollViewReader {proxy in
VStack {
Spacer()
List {
ForEach(floors) { i in
Section(header: Text("Floor \(i.name)")) {
ForEach(unitlines) { u in
Text(u.name)
.padding(.trailing,200)
.contentShape(Rectangle())
.onTapGesture {
print("tapped")
presentationMode.wrappedValue.dismiss()
}
}
}
}
}
HStack {
Spacer()
Text("Flr: \(Int(sliderValue))")
.font(.title)
.padding(10)
Button {
if sliderValue>8 {
sliderValue=sliderValue-1.0
proxy.scrollTo(String(Int(sliderValue)), anchor: .topLeading)
}
else { print("slider below (\(sliderValue))") }
print(proxy)
} label: {
Image(systemName: "minus")
}
.frame(width:30, height:15)
.padding(10)
.background(Color.red)
.clipShape(Capsule())
Slider(value: $sliderValue, in: 8...42, step: 1.0, onEditingChanged: {_ in
withAnimation {
proxy.scrollTo(String(Int(sliderValue)), anchor: .topLeading)
print(String(Int(sliderValue)))
}
}
)
.background(Color.cyan)
.border(Color.blue, width: 1)
.padding(10)
Button {
if sliderValue<42 {
print(sliderValue)
sliderValue=sliderValue+1.0
print(sliderValue)
proxy.scrollTo(String(Int(sliderValue)), anchor: .topLeading)
}
} label: {
Image(systemName: "plus")
}
.frame(width:30, height:15)
.padding(10)
.background(Color.green)
.clipShape(Capsule())
Spacer()
}
.background(Color.gray)
}
}
.navigationTitle("Select Address")
}
}
}

Navigation Bar buttons not working after sheet dismissed. UINavigation wrapper?

I am working on a SwiftUI app, and I have a NavigationView with some buttons in the navigation bar. The problem is that after dismissing a full-page sheet (triggered by a button, not in the navigation bar), those buttons stop working.
I've tried the following, but some TestFlight users say the problem persists (I can't reproduce it myself):
Add an id to each button and change it after the dismiss (I even added it to the toolbar) to force a repaint
Add a height to buttons and navbar
Add #Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> to the presenting and presented views
Set the navigation bar title display mode to inline
I saw an answer on a similar post suggesting wrapping the navigation in a UINavigation. But how do you go about that? I have wrapped views (UITextView), but do you need to wrap the controller? or the navigationItem? or just the buttons. The answer didn't elaborate.
It only seems to happen when the sheet is presented by a button outside the navigation bar. The buttons in the navigation bar also present sheets and they cause no issues. I'm tempted to just hide the navbar altogether and fake it with a regular view.
Just in case you want to see what I have, here's the relevant code in my presenting view (I removed some unrelated content and functionality):
struct PListView: View {
//https://stackoverflow.com/questions/58837007/multiple-sheetispresented-doesnt-work-in-swiftui
enum ActiveSheetProjectList: Identifiable {
case help, settings
var id: Int {
hashValue
}
}
enum ActiveFullSheetProjectList: Identifiable {
case addProject, quickCount
var id: Int {
hashValue
}
}
#ObservedObject var viewModel : ProjectListViewModel
#State var presentingDeleteProjectSheet = false
#State var itemsToDelete : [UUID]?
#State var activeSheet: ActiveSheetProjectList?
#State var activeFullSheet : ActiveFullSheetProjectList?
#ObservedObject var settings : Settings
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
init(settings: Settings) {
self.viewModel = ProjectListViewModel()
self.settings = settings
//https://medium.com/#francisco.gindre/customizing-swiftui-navigation-bar-8369d42b8805
// this is not the same as manipulating the proxy directly
let appearance = UINavigationBarAppearance()
// this overrides everything you have set up earlier.
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor(Color.navBar)
// this only applies to big titles
appearance.largeTitleTextAttributes = [
.font : UIFont.systemFont(ofSize: 20),
NSAttributedString.Key.foregroundColor : UIColor(Color.smallTextMain)
]
// this only applies to small titles
appearance.titleTextAttributes = [
.font : UIFont.systemFont(ofSize: 20),
NSAttributedString.Key.foregroundColor : UIColor(Color.smallTextMain)
]
//In the following two lines you make sure that you apply the style for good
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().standardAppearance = appearance
// This property is not present on the UINavigationBarAppearance
// object for some reason and you have to leave it til the end
UINavigationBar.appearance().tintColor = UIColor(Color.smallTextMain)
}
var body: some View {
NavigationView {
ZStack {
Color.background.edgesIgnoringSafeArea(.all)
VStack {
List {
ForEach(projects) { project in
NavigationLink(destination: ProjectView(project: project, settings: settings, viewModel: ProjectListViewModel(), viewContext: viewContext)
.environmentObject(self.settings)
{
HStack {
Text(project.name ?? "").font(.headline).padding(.bottom, 5).padding(.top, 5)
}
}
.listRowInsets(.init(top: 10, leading: 3, bottom: 10, trailing: 3))
.accessibilityHint(Text(NSLocalizedString("View project details", comment: "")))
}
.onDelete(perform: { indexSet in
presentingDeleteProjectSheet = true
itemsToDelete = indexSet.map { projects[$0].id! }
})
.listRowBackground(Color.lightGray)
.padding(0)
.actionSheet(isPresented: $presentingDeleteProjectSheet) {
var name = NSLocalizedString("Project", comment: "Generic label")
if let id = itemsToDelete?.first {
name = projects.first(where: {$0.id == id})?.name ?? ""
}
return ActionSheet(title: Text(NSLocalizedString(String.localizedStringWithFormat("Delete %#", name), comment: "alert title")), message: Text(NSLocalizedString("Deleting a project can't be undone", comment: "Deleting alert message")), buttons: [
.destructive(Text(NSLocalizedString("Delete", comment: "Button label"))) {
if itemsToDelete != nil {
viewModel.deleteProjects(projects: activeProjectsDateCreated, ids: itemsToDelete!)
}
},
.cancel({itemsToDelete?.removeAll()})
])
}
}
.padding(0)
.onAppear(perform: {
UITableView.appearance().backgroundColor = UIColor(Color.lightGray)
UITableViewCell.appearance().selectionStyle = .none
})
}
}
}
.fullScreenCover(item: $activeFullSheet, content: { item in
switch item {
case .quickCount :
// THIS IS THE SHEET THAT CAUSES THE ISSUES
QuickCountView(viewModel: CounterViewModel(counter: viewModel.getScratchCounter(projects: quickCountProject), sound: settings.sound, showTotal: settings.showTotal, viewContext: viewContext))
.environmentObject(settings)
case .addProject:
// No problems after dismissing this one
AddEditProjectView(viewModel: AddEditProjectViewModel(project : nil, startAt: settings.startNumber, viewContext: viewContext), isNew: true, isEditing: .constant(true))
.environmentObject(settings)
}
})
Button(action: { activeFullSheet = .quickCount }, label: {
Text(NSLocalizedString("Quick Count +", comment: "Button label"))
.accessibilityLabel(NSLocalizedString("Quick count", comment: ""))
})
.buttonStyle(CustomButton(style: .button, size: .large))
.padding()
.sheet(item: $activeSheet) { item in
switch item {
case .help:
HelpView()
case .settings:
SettingsView()
.environmentObject(settings)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
HStack {
Button(action: {
self.activeSheet = .settings
}) {
Image(systemName: "gearshape.fill")
.font(Font.system(size: 28, weight: .medium, design: .rounded))
.foregroundColor(Color.main)
.accessibilityLabel(Text(NSLocalizedString("Settings", comment: "a11y label")))
.frame(height: 96, alignment: .trailing)
}
Button(action: {
self.activeSheet = .help
}) {
Image(systemName: "questionmark")
.font(Font.system(size: 28, weight: .semibold, design: .rounded))
.foregroundColor(Color.main)
.accessibilityLabel(Text(NSLocalizedString("Help", comment: "a11y label")))
.frame(height: 96, alignment: .trailing)
}
}
}
ToolbarItemGroup(placement: .navigationBarTrailing) {
HStack {
Button(action: { activeFullSheet = .addProject }) {
Image(systemName: "plus")
.font(Font.system(size: 30, weight: .semibold))
.foregroundColor(Color.main)
.accessibilityLabel(Text(NSLocalizedString("Add a Project", comment: "a11y label")))
.frame(height: 96, alignment: .trailing)
}
Button(action: {
self.isEditing.toggle()
}) {
Image(systemName: isEditing ? "xmark" : "pencil")
.font(Font.system(size: 28, weight: .black))
.foregroundColor(activeProjectsDateCreated.count >= 1 ? Color.main : Color.gray)
.accessibilityLabel(Text(NSLocalizedString("Edit Project List", comment: "a11y label")))
.frame(height: 96, alignment: .trailing)
}.disabled(activeProjectsDateCreated.count < 1)
.frame(height: 96, alignment: .trailing)
}
}
}
}
}

SwiftUI NavigationLink not behaving correctly when buttons are close together

I'm trying to implement a programmable NavigationLink to go to another view, triggered by a simple button. But when that button is next to another button, instead of triggering the NavigationLink, it triggers the action of the button next to it.
struct NavLinkView: View {
#State var showPasswordStr = true
#State var showCheckView = false
#State var password = "Abc"
var body: some View {
Form {
VStack {
NavigationLink(destination: CheckView(), isActive: $showCheckView ) {
EmptyView()
}
Text("Password")
.font(.system(size: 12, weight: .light, design: .default))
.foregroundColor(.gray)
HStack {
if showPasswordStr {
TextField("", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
} else {
SecureField("", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: {
showPasswordStr.toggle()
} ) {
Image(systemName: showPasswordStr ? "eye.slash" : "eye" )
}
.padding(.leading)
Button(action: { showCheckView.toggle() } ) {
Image(systemName: "checkmark.circle" )
}
.padding(.leading)
}
}
}
}
}
What am I mising? If I move the NavigationLink to be after the VStack, then both buttons trigger both the action of the first button and the NavigationLink.
As your buttons are in a Form you need to change their style to PlainButtonStyle:
Button(action: {
self.showPasswordStr.toggle()
}) {
Image(systemName: showPasswordStr ? "eye.slash" : "eye")
}
.buttonStyle(PlainButtonStyle()) // <- add style
Button(action: { self.showCheckView.toggle() }) {
Image(systemName: "checkmark.circle")
}
.buttonStyle(PlainButtonStyle()) // <- add style

SwiftUI Popover Size is not expanding to fit content

Here is my code
struct ContentView: View {
#State var showingPopover = false
var body: some View {
VStack {
Spacer()
Text("Hello World")
Spacer()
HStack {
Spacer()
Button {
self.showingPopover.toggle()
} label: {
Image(systemName: "plus.circle")
}
.popover(isPresented: $showingPopover) {
List(0..<100) { Text("\($0)") }
}.padding(30)
}
}
}
}
This should produce a really nice popover coming from the plus button. But all I get is a really squashed down popover.
Any idea what I am missing here? Is there a way to tell the popover to expand more (without specifying a size)?
You may use a ScrollView and ForEach instead of a List:
struct ContentView: View {
#State var showingPopover = false
var body: some View {
VStack {
Spacer()
Text("Hello World")
Spacer()
HStack {
Spacer()
Button(action: {
self.showingPopover.toggle()
}) {
Image(systemName: "plus.circle")
}
.padding(30)
}
}
// can be attached to the button as well (as in the question)
.popover(isPresented: $showingPopover,
attachmentAnchor: .point(.bottomTrailing),
arrowEdge: .bottom) {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0 ..< 100) {
Text("\($0)")
}
}
}
}
}
You can provide a custom frame for the List. Also, don't forget to embed List inside a ScrollView if you want it to scroll.
ScrollView {
List(0..<100) {
Text("\($0)")
}
.frame(width: 100, height: 250)
}

How to perform an action after NavigationLink is tapped?

I have a Plus button in my first view. Looks like a FAB button. I want to hide it after I tap some step wrapped in NavigationLink. So far I have something like this:
ForEach(0 ..< 12) {item in
NavigationLink(destination: TransactionsDetailsView()) {
VStack {
HStack(alignment: .top) {
Text("List item")
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
}
.simultaneousGesture(TapGesture().onEnded{
self.showPlusButton = false
})
.onAppear(){
self.showPlusButton = true
}
}
It works fine with single tap. But when I long press NavigationLink it doesn't work. How should I rewrite my code to include long press as well? Or maybe I should make it work different than using simultaneousGesture?
I'm using the following code. I prefer it to just NavigationLink by itself because it lets me reuse my existing ButtonStyles.
struct NavigationButton<Destination: View, Label: View>: View {
var action: () -> Void = { }
var destination: () -> Destination
var label: () -> Label
#State private var isActive: Bool = false
var body: some View {
Button(action: {
self.action()
self.isActive.toggle()
}) {
self.label()
.background(
ScrollView { // Fixes a bug where the navigation bar may become hidden on the pushed view
NavigationLink(destination: LazyDestination { self.destination() },
isActive: self.$isActive) { EmptyView() }
}
)
}
}
}
// This view lets us avoid instantiating our Destination before it has been pushed.
struct LazyDestination<Destination: View>: View {
var destination: () -> Destination
var body: some View {
self.destination()
}
}
And to use it:
var body: some View {
NavigationButton(
action: { print("tapped!") },
destination: { Text("Pushed View") },
label: { Text("Tap me") }
)
}
Yes, NavigationLink does not allow such simultaneous gestures (might be as designed, might be due to issue, whatever).
The behavior that you expect might be implemented as follows (of course if you need some chevron in the list item, you will need to add it manually)
struct TestSimultaneousGesture: View {
#State var showPlusButton = false
#State var currentTag: Int?
var body: some View {
NavigationView {
List {
ForEach(0 ..< 12) { item in
VStack {
HStack(alignment: .top) {
Text("List item")
NavigationLink(destination: Text("Details"), tag: item, selection: self.$currentTag) {
EmptyView()
}
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
.simultaneousGesture(TapGesture().onEnded{
print("Got Tap")
self.currentTag = item
self.showPlusButton = false
})
.simultaneousGesture(LongPressGesture().onEnded{_ in
print("Got Long Press")
self.currentTag = item
self.showPlusButton = false
})
.onAppear(){
self.showPlusButton = true
}
}
}
}
}
}
Another alternative I have tried. Not using simultaneousGesture, but an onDisappear modifier instead. Code is simple and It works. One downside is that those actions happen with a slight delay. Because first the destination view slides in and after this the actions are performed. This is why I still prefer #Asperi's answer where he added .simultaneousGesture(LongPressGesture) to my code.
ForEach(0 ..< 12) {item in
NavigationLink(destination: TransactionsDetailsView()) {
VStack {
HStack(alignment: .top) {
Text("List item")
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
}
.onDisappear(){
self.showPlusButton = false
}
.onAppear(){
self.showPlusButton = true
}
}
I have tried an alternative approach to solving my problem. Initially I didn't use "List" because I had a problem with part of my code. But it cause another problem: PlusButton not disappearing on next screen after tapping NavigationLink. This is why I wanted to use simultaneousGesture - after tapping a link some actions would be performed as well (here: PlusButton would be hidden). But it didn't work well.
I have tried an alternative solution. Using List (and maybe I will solve another problem later.
Here is my alternative code. simultaneousGesture is not needed at all. Chevrons are added automatically to the list. And PlusButton hides the same I wanted.
import SwiftUI
struct BookingView: View {
#State private var show_modal: Bool = false
var body: some View {
NavigationView {
ZStack {
List {
DateView()
.listRowInsets(EdgeInsets())
ForEach(0 ..< 12) {item in
NavigationLink(destination: BookingDetailsView()) {
HStack {
Text("Booking list item")
Spacer()
}
.padding()
}
}
}.navigationBarTitle(Text("Booking"))
VStack {
Spacer()
Button(action: {
print("Button Pushed")
self.show_modal = true
}) {
Image(systemName: "plus")
.font(.largeTitle)
.frame(width: 60, height: 60)
.foregroundColor(Color.white)
}.sheet(isPresented: self.$show_modal) {
BookingAddView()
}
.background(Color.blue)
.cornerRadius(30)
.padding()
.shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3)
}
}
}
}
}