Better way of handling Tap Gesture on Lists - swift

I'm trying to implement a 1 count tap gesture and 2 count tap gesture on rows of a list view.
Currently I'm achieving this by expanding the contents of the rows to fill the entire width and height, by changing the row insets with .listRowInsets(EdgeInsets()) and adding an HStack with a Spacer inside. As seen below:
My ListRow View, which serves to attempt to fill out the the entire rows' width and height. (With a red border for debugging):
struct ListRow<Content: View> : View {
let content: () -> Content
var body: some View {
HStack {
content()
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.border(Color.red)
}
}
And here's a snippet of one of the two places I'm using a list with my ListRow:
List(selection: $selectedItem) {
Section(header: Text("Favourites")) {
ForEach($sidebarItems, id: \.self) { $item in
ListRow {
FileLabel(URL(string: item.path)!, size: 14, text: item.text)
}.gesture(TapGesture().onEnded {
selectedItem = item
fileState.updatePath(path: item.path)
}).listRowInsets(EdgeInsets())
}
}
}.listStyle(SidebarListStyle())
A few issues happen here, which both feels hacky and incorrect to do.
I have to manually select the row when the content is tapped.
I actually manually have to focus the list, if it's not actually focused when clicking a row, otherwise the selection is a very faint color.
The contents aren't actually expanding to the leading and trailing edges of the row, no matter if I give my EdgeInsets negative values (This just results in the contents getting clipped, rather than expanding to the edge).
Clicking the areas outside of the red border (seen below) results in the row being selected, but the tap action not firing.
Preferably I'd love if there was a way to listen for tap gestures on a list and get the row that was tapped. It would fix having to manually reimplement features that the List already has built-in and lowering the risk of introducing bugs + it just feels like the right way to do it, unfortunately I haven't been able to find other ways of implementing this, other than creating an HStack with a Spacer.
EDIT:
I'm curious as to how NavigationLink works as no matter where you click, or if you navigate using your keyboard, it'll actually trigger the navigation. Makes me think there's some kind of binding or event fired?

Related

Exclude button from disabling

I have custom view with different content inside including Button.
For example:
VStack {
Text("Some text")
Button() // 1-th
Button() // 2-th
.disabled(false) // This doesn't help, despite it logical
}
.disabled(true) // Line with disabling
I want 2-th button to be always enabled no matter what is set in "Line with disabling".
According to the documentation this does not work that way. It works the other way around. As you allready discovered the .disable on the parent overrides the value set in the child.
Possible solution would be to not use the parent .disable instead use it on each child element.

How to resize List items with a sidebar style

I want to reduce the size of the items in a List view, more precisely the height, the list being styled like a sidebar (.listStyle(.sidebar)). I tried changing the size with .controlSize(.mini) but it didn't work. It worked for other list styles (plain, bordered, etc.).
What strikes me is that Xcode sidebar does have list items that are smaller than the regular size, so it should be possible !
Side by side comparison between Xcode sidebar and my app sidebar
Is there a simple and idiomatic way to do this ?
Apologies if I am not correctly understanding the question but can't you just set the frame of the items? For example:
struct SwiftUIView: View {
var body: some View {
List {
ForEach((1...10), id: \.self) {_ in
Text("hello")
.frame(height: 50) // <- Right here!
}
}
.listStyle(.sidebar)
}
}
Where 50 would be the height you want.

SwiftUI question about .padding view modifier from a lecture I saw

So I was watching a lecture and I saw this:
Modifiers Remember that viewmodifier functions(like .padding)
themselves return a View. That view, conceptually anyway, "contains"
the View it's modifying.
Many of them just pass the size offered to them along(like .font or
.foregroundColor). But it is possible for a modifier to be involved in
the layout process itself.
For example the view returned by .padding(10) will offer the view that
it is modifying a space that that is the same size as it was offered,
but reduced by 10 points on each side. The view returned by
.padding(10) would then choose a size for itself which is 10 points
larger on all sides than the View it is modifying ended up choosing.
I don't fully understand this description because if I have some View I call T and I wanna add padding with T.padding(10) What is this saying will happen because this description doesn't fully make sense to me could someone maybe try to explain it a bit better..? Even though this does seem like a simple explanation to me it doesn't really click.
Edit for screenshot:
Think about this as calling .padding(10) on T, creates T1 view having 10pt larger size at each edge, places T at the center of this new T1 view, and returns new T1 view.
Thus every modifier can be considered as a factory of new container view which encloses original view and can affect it on purpose.
In the example you provide, the HStack is a View that is made up of Card Views. The .foregroundColor(Color.orange) modifier takes the HStack View and returns a new View. Finally, the .padding(10) modifier takes that orange View and returns a new View adding padding of size 10 on all sides.
This slide is then explaining how the layout works, which is in the reverse direction. The first View in the chain of Views to be offered space is the last one, that is the View that results from adding padding. For instance, this View might be offered the entire screen if this were the top View in the ContentView. The padding view for layout purposes would then subtract 10 from all sides and offer the smaller space to the .foregroundColor() modifier. Since .foregroundColor() doesn't affect layout, it would offer this same size to the HStack View. The HStack would divide the offered space by the number of cards in the ForEach and offer this smaller view size to each of the Card Views.
An Example With Numbers
It might help your understanding to look at an example with real numbers.
Suppose this total view will fit into a space 200 wide by 240 tall, and there are 2 cards in the HStack. How big will each Card be? This is the question Layout is figuring out.
If you have 200 by 240 to work with, then the padding is going to take up space on all sides leaving a space of only 180 by 220 for the HStack to lay out its views. The HStack determines that it has 2 views laid out horizontally, so it offers half the space to each of the card views (90 by 220).
So, the padding is actually going to make the Card Views smaller because it is taking up some of the existing space.
Example to Play With in an iOS App
Try the following code in an iOS app. Run it as is and see what it looks like. Then change the padding to 100 and run it again. Notice that as the padding gets larger, the red and blue views get smaller.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Color.red
Color.blue
}
.padding(10)
}
}
More Extensive Example with Slider to Adjust Padding
Here is a interactive example that shows the padding in Color.green that can be adjusted with the slider. The dimensions of the red and blue views are shown using GeometryReader.
struct ContentView: View {
#State private var pad: CGFloat = 50
var body: some View {
VStack {
HStack {
GeometryReader { geo in
ZStack {
Color.red
Text("\(geo.size.width, specifier: "%.1f")x\(geo.size.height, specifier: "%.1f")").foregroundColor(.white)
}
}
GeometryReader { geo in
ZStack {
Color.blue
Text("\(geo.size.width, specifier: "%.1f")x\(geo.size.height, specifier: "%.1f")").foregroundColor(.white)
}
}
}
.background(Color.yellow)
.padding(pad)
}
.background(Color.green)
HStack {
Text("Padding =")
HStack {
Text("\(pad, specifier: "%.1f")")
Spacer()
}.frame(width: 50)
}.foregroundColor(.green)
Slider(value: $pad, in: 0...100).padding(.horizontal)
}
}

SwiftUI on Mac: Help Text Always Visible Even within View with Zero Opacity

I have run into some unexpected behavior while using SwiftUI in a macOS app. I filed a Feedback with Apple in case it's a bug, but it might actually be designed to work this way, so I'm looking for a workaround.
I rely heavily on the use of .opacity() to show and hide different sections of my app with tabs. I don't use if clauses because each time the user changes the tab, you have to wait for the entire view to rebuild and that is pretty slow.
Here's a basic example that demonstrates the problem:
struct ContentView: View {
#State var viewAVisible = false
var body: some View {
VStack{
ZStack{
Text("View A Visible")
.frame(width: 500, height: 500)
.background(Color.blue)
.help("This is View A's help text. It should be invisible when View A is invisible.")
.opacity(viewAVisible ? 1 : 0)
Text("View B Visible")
.frame(width: 500, height: 500)
.background(Color.gray)
.opacity(viewAVisible ? 0 : 1)
}
Button("Toggle"){
viewAVisible.toggle()
}
}.padding()
}
}
The default app state is to hide the "View A" Text() and only show the "View B" Text(). But if you hover over View B, you still see View A's .help text:
In my opinion, if a view has .opacity(0) then its help text shouldn't show up. But regardless, I need to find a way around this.
I thought about doing something like this:
.help(viewAVisible ? "This is View A's help text..." : "")
...but that doesn't scale across dozens of views in my app--particularly among child views that don't know if their parent view is shown or hidden. As I mouse across my app, I see the help text of tons of views all over the place even though they are invisible. 😅
Has anyone run into this or have any suggestions on how to handle it?
Looks like a bug (they do not remove tracking rects), here is a demo of workaround - move help tag into background and remove it manually (tested with macOS 12.0.1)
Text("View A Visible")
.frame(width: 500, height: 500)
.background(Group {
if viewAVisible {
Color.blue.help("This is View A's help text. It should be invisible when View A is invisible.")
} else {
Color.clear
}
})
.opacity(viewAVisible ? 1 : 0)

Is there a way to keep a SwiftUI text label from slightly moving while displaying a timer?

I have a simple HStack containing 2 Text structs displaying a timer. It keeps changing position slightly while the timer value changes. Is there a way to avoid this behavior.
struct DurationLabel: View {
let majorDuration: String // the hours/minutes part "00:00"
let minorDuration: String // the seconds part "00"
var body: some View {
HStack(spacing: 0) {
Text(majorDuration + minorDuration)
.font(.system(size: 90))
Text(minorDuration)
.font(.body)
}
}
}
For displays like this I prefer to use a font with monospaced digits. You can use them in your Text like so:
Text("your Text here").font(Font.largeTitle.monospacedDigit())
I've done this a number of times for audio players, etc... If you want to keep the same font, you can do something like:
ZStack(alignment: .leading) {
Text("00:00").opacity(0.0)
Text("MM:SS") // Put your actual numbers in here with the same formatting.
}
The ZStack will size itself based on the largest subview, and 0 is the widest digit in basically any font.
With .center alignment this will keep surrounding views from shifting around each second. With .leading, you keep the label itself from shifting too much.
This technique works for any type of content that may change size. Just load up a "dummy" view with the largest possible version and hide it. Then keep your real content in the visible view. Then you can avoid hardcoding a frame size, etc...