SwiftUI Button cannot be clicked on its whole area - swift

I built a custom Button.
My problem is, that it can be only clicked on the Text Elements not on the whole area of the Button.
What's going wrong, how can I solve this?
struct MyButton: View {
var body: some View
{ Button( action:{ print("pressed") })
{ HStack {
VStack(alignment: .leading){
Text("Tomorrow").font(.caption)
Text("23.5.22 KW 23")
}
Spacer()
}
}
.padding([.horizontal],4.0)
.padding([.vertical],2.0)
.background(RoundedRectangle(cornerRadius: 5).fill(Color.red))
.buttonStyle(PlainButtonStyle())
.padding([.horizontal],8.0)
}
}

By default, only the parts of the view that actually render something are tappable. You need to add a .contentShape(Rectangle()) modifier to your Button to make the entire area interactive.

Related

Color in ZStack not behaving properly

so I'd like to lay a Color at the top of a ZStack over another view. The reason I don't want to use overlay is the Color is going to have a tap gesture attached to it. A minimal reproducible example is below. Essentially, I want the Color.secondary to be confined to the same area as the HStack (without explicitly setting frame size. Here's the code:
struct ContentView: View {
var body: some View {
ZStack {
HStack {
Spacer()
Button(action: {
print("tapped button")
}, label: {
Text("button")
})
}.background(Color.red)
Color.secondary
.onTapGesture {
print("clicked color")
}
}
}
}
So I'd like the view to just be a white screen, with an HStack that looks slightly darker red.
Below is a picture of my UI. The view is greyed out during onboarding, and the user will essentially just tap the grey area to go to the next step in the onboarding. If I attach a tap gesture to the Color view and then just hide the color view according to state changes, this isn't a problem. After the onboarding is completed, the greyed area won't be there and the buttons underneath need to be interactable. When using overlays, AFTER onboarding, I don't want tapping anywhere on the view to change the app state.
https://i.stack.imgur.com/lphHg.png
Given your further description, I believe you have the onTapGesture(perform:) in the wrong place, outside of the overlay rather than inside.
In the code below, the onTapGesture(perform:) can only be tapped on the gray overlay when it is showing. However when it is attached after the overlay, it can be tapped on when not tapping on something like the button.
Working code:
struct ContentView: View {
#AppStorage("is_onboarding") var isOnboarding = true
var body: some View {
HStack {
Spacer()
Button {
print("tapped button")
} label: {
Text("button")
}
}
.background(Color.red)
.overlay {
if isOnboarding {
Color.secondary
.onTapGesture {
print("clicked color")
isOnboarding = false
}
}
}
}
}
If on iOS 14+ not iOS 15+, use the following overlay instead:
.overlay(
isOnboarding ?
Color.secondary
.onTapGesture {
print("clicked color")
isOnboarding = false
}
: nil
)

Add Padding to TabView Page Indicator

I have a view that is a TabView with the style PageTabViewStyle(indexDisplayMode: .always which looks great for my use case however the page indicator is bumping right up to the safe area near the bottom of the screen and it looks bad. I'd like to move the page indicator up to some n value. here's a sample of my view to reproducing it. If this view were built on any device without a Home Button, it will ride on top of the home indicator line.
var body: some View {
ZStack {
TabView(selection: $homeVM.selectedPageIndex) {
// Any number of views here.
}
.frame(width: UIScreen.main.bounds.width)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .never))
}.edgesIgnoringSafeArea(.all)
}
I attempted to add the padding to the ZStack, which does work but then my TabView is cut off on the bottom, which means my cells disappear prematurely.
Here's an image for what I'm trying to fix. Notice the page indicator sits on the home bar indicator. I need the indicators pushed up, without pushing up the background ScrollView
Update #1
This view is being presented by a base view that I use to handle my navigation stack. The view is as follows. The important thing to note here is the .ignoresSafeArea() that I have on this view. I did that because it's a containing view for my eventual TabView to be presented from. Interestingly if I remove this modifier the indicators move up to a more manageable position, but then my form becomes clipped at the top and bottom of the device when scrolling, and that's not ideal.
struct BaseLaunchView: View {
#StateObject var baseNavVM = BaseLaunchViewModel()
#State var shouldLogin = false
#State var shouldRegister = false
var body: some View {
VStack {
switch baseNavVM.loggedIn {
case true:
HomeView()
default:
NavigationView {
VStack{
Spacer()
VStack(spacing: 30) {
VStack {
Text("Stello")
.font(Fonts.title)
Text("Life Groups")
.font(Fonts.body)
}
StelloDivider()
Text("Connect with like minded people, to fellowship and find your home.")
.font(Fonts.subheading)
.multilineTextAlignment(.center)
}
Spacer()
NavigationLink(destination: RegisterOptionsView(isLoggingIn: true), isActive: $shouldLogin) {
Button(action: {
shouldLogin.toggle()
}, label: {
Text("Login")
.font(Fonts.button)
}).buttonStyle(StelloFillButtonStyle())
}
NavigationLink(destination: RegisterOptionsView(isLoggingIn: false), isActive: $shouldRegister) {
Button(action: {
shouldRegister.toggle()
}, label: {
Text("Register")
.font(Fonts.button)
}).buttonStyle(StelloHollowButtonStyle())
}
}
}.accentColor(.black)
}
}
.padding()
.environmentObject(baseNavVM)
.ignoresSafeArea()
}
}

How come my NavigationLink won't work when clicked?

I am trying to make it so that when I click the icon, the "scoreView()" is opened. When I click it, nothing works right now. Here is the code:
HStack {
Image(systemName: "arrow.counterclockwise")
NavigationLink(destination: scoreView(scoreTracker: $scoreTracker)) {
Spacer()
Image(systemName: "list.bullet")
}
}
Does it have something to do with the fact that I don't have a navigationView? I'm new to this and experimenting so I'm not very clear on it.
EDIT:
I have added a NavigationView, yet the NavigationLink covers half the screen, and when clicked, the view is only changed in that square.
Before clicking the NavigationLink
After clicking the NavigationLink
HStack {
Image(systemName: "arrow.counterclockwise")
NavigationView {
NavigationLink(destination: scoreView(scoreTracker: $scoreTracker)) {
Image(systemName: "list.bullet")
}
}
}
Does it have something to do with the fact that I don't have a navigationView?
Yes. According to the documentation:
Users click or tap a navigation link to present a view inside a NavigationView.
It will only work inside a NavigationView. If you're not using one, consider sheet or fullScreenCover instead. Or, make your own overlay with a ZStack.
Example NavigationView usage:
struct ContentView: View {
var body: some View {
NavigationView { /// directly inside `var body: some View`
VStack { /// if you have multiple views, make sure to put them in a `VStack` or similar
Text("Some text")
/// `ScoreView` should be capitalized
NavigationLink(destination: ScoreView(scoreTracker: $scoreTracker)) {
Image(systemName: "list.bullet")
}
}
}
}
}

SwiftUI List disclosure indicator without NavigationLink

I am searching for a solution to show the disclosure indicator chevron without having the need to wrap my view into an NavigationLink. For example I want to show the indicator but not navigate to a new view but instead show a modal for example.
I have found a lot solutions that hide the indicator button but none which explains how to add one. Is this even possible in the current SwiftUI version ?
struct MyList: View {
var body: some View {
NavigationView {
List {
Section {
Text("Item 1")
Text("Item 2")
Text("Item 3")
Text("Item 4")
}
}
}
}
For example I want to add the disclosure indicator to Item 1 without needing to wrap it into an NavigationLink
I already tried to fake the indicator with the chevron.right SF Symbol, but the symbol does not match 100% the default iOS one. Top is default bottom is chevron.right.
It is definitely possible.
You can use a combination of Button and a non-functional NavigationLink to achieve what you want.
Add the following extension on NavigationLink.
extension NavigationLink where Label == EmptyView, Destination == EmptyView {
/// Useful in cases where a `NavigationLink` is needed but there should not be
/// a destination. e.g. for programmatic navigation.
static var empty: NavigationLink {
self.init(destination: EmptyView(), label: { EmptyView() })
}
}
Then, in your List, you can do something like this for the row:
// ...
ForEach(section.items) { item in
Button(action: {
// your custom navigation / action goes here
}) {
HStack {
Text(item.name)
Spacer()
NavigationLink.empty
}
}
}
// ...
The above produces the same result as if you had used a NavigationLink and also highlights / dehighlights the row as expected on interactions.
Hopefully, this is what you are looking for. You can add the item to a HStack and with a Spacer in between fake it that its a Link:
HStack {
Text("Item 1")
Spacer()
Button(action: {
}){
Image(systemName: "chevron.right")
.font(.body)
}
}
The answers already submitted don't account for one thing: the highlighting of the cell when it is tapped. See the About Peek-a-View cell in the image at the bottom of my answer — it is being highlighted because I was pressing it when the screenshot was taken.
My solution accounts for both this and the chevron:
Button(action: { /* handle the tap here */ }) {
NavigationLink("Cell title", destination: EmptyView())
}
.foregroundColor(Color(uiColor: .label))
The presence of the Button seems to inform SwiftUI when the cell is being tapped; simply adding an onTapGesture() is not enough.
The only downside to this approach is that specifying the .foregroundColor() is required; without it, the button text will be blue instead.
in iOS15 the following is a better match as the other solutions were little too big and not bold enough. it'll also resize better to different Display scales better than specifying font sizes.
HStack {
Text("Label")
Spacer()
Image(systemName: "chevron.forward")
.font(Font.system(.caption).weight(.bold))
.foregroundColor(Color(UIColor.tertiaryLabel))
}
Would be good if there was an offical way of doing this. Updating every OS tweak is annoying.
I found an original looking solution. Inserting the icon by hand does not bring the exact same look.
The trick is to use the initializer with the "isActive" parameter and pass a local binding which is always false. So the NavigationLink waits for a programmatically trigger event which will never occur.
// use this initializer
NavigationLink(isActive: <Binding<Bool>>, destination: <() -> _>, label: <() -> _>)
You can pass an empty closure to the destination parameter. It will never get called anyway. To do some action you put a button on top within a ZStack.
func navigationLinkStyle() -> some View {
let never = Binding<Bool> { false } set: { _ in }
return ZStack {
NavigationLink(isActive: never, destination: { }) {
Text("Item 1") // your list cell view
}
Button {
// do your action on tap gesture
} label: {
EmptyView() // invisible placeholder
}
}
}
For accessibility you might need to mimic UIKit version of disclosure indicator. You don't need to implement it this way per se but if you use e.g. Appium for testing you might want to have it like this to keep tests succeeding
Apparently UIKit's disclosure indicator is a disabled button with some accessibility values so here's the solution:
struct DisclosureIndicator: View {
var body: some View {
Button {
} label: {
Image(systemName: "chevron.right")
.font(.body)
.foregroundColor(Color(UIColor.tertiaryLabel))
}
.disabled(true)
.accessibilityLabel(Text("chevron"))
.accessibilityIdentifier("chevron")
.accessibilityHidden(true)
}
}
Or maybe create a fake one and use it, even if you tap you can call your events.
NavigationLink(destination: EmptyView()) {
HStack {
Circle()
Text("TITLE")
}
}
.contentShape(Rectangle())
.onTapGesture {
print("ALERT MAYBE")
}
I created a custom NavigationLink that:
Adds an action API (instead of having to push a View)
Shows the disclosure indicator
Ensures that List cell selection remains as-is
Usage
MYNavigationLink(action: {
didSelectCell()
}) {
MYCellView()
}
Code
import SwiftUI
struct MYNavigationLink<Label: View>: View {
#Environment(\.colorScheme) var colorScheme
private let action: () -> Void
private let label: () -> Label
init(action: #escaping () -> Void, #ViewBuilder label: #escaping () -> Label) {
self.action = action
self.label = label
}
var body: some View {
Button(action: action) {
HStack(spacing: 0) {
label()
Spacer()
NavigationLink.empty
.layoutPriority(-1) // prioritize `label`
}
}
// Fix the `tint` color that `Button` adds
.tint(colorScheme == .dark ? .white : .black) // TODO: Change this for your app
}
}
// Inspiration:
// - https://stackoverflow.com/a/66891173/826435
private extension NavigationLink where Label == EmptyView, Destination == EmptyView {
static var empty: NavigationLink {
self.init(destination: EmptyView(), label: { EmptyView() })
}
}

Why .gesture() in SwiftUI can not be used on my entire modal view?

I have this really simple LostPasswordView using SwiftUI. This form is being presented as a modal view. I use a .onTapGesture and a DragGesture() to dismiss the keyboard.
The issue is that only the little part of the view around the TextField responds to these gestures. Which does not improve the user experience as it is confusing on where these actions work.
Why can I not use the gesture on all the SwiftUI modal View? I was thinking that it might be because there is already a swipe down gesture to dismiss the modal view...
import SwiftUI
//MARK: Lost password view presented as a modal
struct LostPasswordView: View {
#State var email = ""
var body: some View {
//MARK: User entry
VStack {
///email
UserEntryTextField(title: TextFieldName.email.rawValue,
userEntry: email
)
.padding(.top, 198)
Spacer()
//MARK: Modify password button
MainButton(action: {},
title: .modifyPassword)
}
.padding(.bottom)
.onTapGesture {
self.dismissKeyboard()
}.simultaneousGesture(
DragGesture().onChanged({ (_) in
self.dismissKeyboard()
}))
}
}
//MARK: Dismiss keyboard
extension LostPasswordView {
func dismissKeyboard() {
UIApplication.shared.dismissKeyboard()
}
}
Specify .frame modifier for your VStack as follows (and probably you need some background, because transparent containers do not track events)
VStack {
...
}
.background(Color.white)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)