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

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)

Related

How do we control the automatic dividers in SwiftUI forms?

I'm curious, is there a way we can control (or make sense of) the automatic dividers that swiftui puts between items?
Here's some code showing strange behaviour (on iOS simulator):
struct FormSeparator: View {
#State private var passwd = ""
var body: some View {
Form {
ZStack(alignment: .trailing) {
TextField("password", text:$passwd)
Text("8 chars min")
.font(.caption)
.foregroundColor(.secondary)
}
TextField("confirm", text:$passwd)
}
}
}
Here's the result:
This seems to have something to do with the Text that I'm stacking on top of the password field. If I change it the Text to Image(systemName: "lock") then everything works as you'd expect:
Mind you, I don't even understand why it doesn't extend all the way across the form in that case. Presumably this is deliberate, has anyone seen anything official about this situation? Do we know how to control this?
In iOS 16, List/Form row separator insets automatically and aligns with the text. In iOS 16, we got two new alignments, listRowSeparatorLeading and listRowSeparatorTrailing, to work with alignmentGuide(_:computeValue:).
ZStack(alignment: .trailing) {
TextField("password", text:$passwd)
Text("8 chars min")
.font(.caption)
.foregroundColor(.secondary)
}
.alignmentGuide(.listRowSeparatorLeading) { viewDimensions in
return 0
}

Better way of handling Tap Gesture on Lists

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?

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)
}
}

How do you create a sheet that follows the macOS sheet style in SwiftUI?

I'm trying to replicate the sheet style that most macOS apps use, but where I'm stuck is making the buttons wide and having one highlighted blue as a suggestion. This is probably explained best through pictures:
Here's what I want:
Picture of a sheet from Xcode asking if I'd like to delete File.swift with 3 large buttons, one of which is highlighted blue
Here's where I am currently:
Picture of a sheet from my app asking if you'd like to reset the generator with two small grey buttons
And here's my code: (minus some variables not relevant to the sheet)
.sheet(isPresented: $confirmReset) {
VStack(alignment: .center) {
Image("sheeticon")
.resizable()
.frame(width: 72, height: 72)
Text("Confirm Reset")
.font(.title2)
Text("Are you sure you want to reset the generator?")
.multilineTextAlignment(.center)
.padding(.horizontal, 4)
Button(action:{
/* Resets some variables first, removed to make this easier to read */
confirmReset = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
}) {
Text("Confirm")
}
.controlSize(.large)
Button(action:{
confirmReset = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
}) {
Text("Cancel")
}
.controlSize(.large)
}
.frame(width: 250, height: 250)
}
How would I got about making my buttons look like the ones used in Xcode (and most other macOS apps)? I've tried searching through any similar question I could find and any documentation I could find but I haven't had any luck so far.
Extra detail I forgot: I'm on macOS Big Sur 11.6 using Xcode 13

SwiftUI TextField text bounds fail to update with GeometryReader when window resized, causing buggy looks

not-ios
The buggy looks include the text looks vertically truncated, and the blue selection border doesn't match. These issues are directly related and is touched upon further in the question.
I have been told that I should ask individual questions when I am experiencing TextField issues. So here is one. The issue is when the text size is dependant on window width, resizing the window gives it a buggy look. Take a look at the screenshots. The code is in a VStack and GeometryReader.
Code:
TextField("World Name", text: self.$WorldName)
.font(.system(size: geometry.size.width/24))
.textFieldStyle(PlainTextFieldStyle())
.padding([.leading, .trailing], 6)
.frame(width: geometry.size.width*0.75, height: geometry.size.width/20)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.init(white: 0.28))
)
TextField("World Seed", text: self.$WorldSeed)
.font(.system(size: geometry.size.width/24))
.textFieldStyle(PlainTextFieldStyle())
.padding([.leading, .trailing], 6)
.frame(width: geometry.size.width*0.75, height: geometry.size.width/20)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.init(white: 0.28))
)
Here are screenshots showing this behavior.
Normal Window:
It looks like this when I resize it:
But as soon as I click on the fields, it looks normal again:
I have also noticed that the blue selection border does not scale. I presume that the blue border shows the border of the text, so this seems directly tied to the truncation of the text.
How would I go about fixing this issue?
A possible approach would be updating the window every time it is resized, but I am reluctant to do that for performance reasons. But if performance is not an issue here, I need to know how to call a function when the window is resized from within the view.
And to make this redundantly clear, I am not creating iOS apps, I am creating macOS apps.
My expected result is for when the window resizes, I want the textfield to immediately look like the third image, where the text bounds match the new size if the textfield, and thus, the blue border will match the textfield, and the text will not appear truncated. What can I add to my code to fix it, in the most efficient way possible? How can I update the textfield bounds with the GeometryReader?
Note: This bug does not happen IF text is TYPED in the TextField. ONLY when it is empty.
Summarised list of notes:
I have created a TextField
TextField's Font size is dependant on the
GeometryReader.
If empty, the TextField's text appears truncated until selected
The bug does not occur if there is text typed in the
textfield
The blue border also does not match the textfield, and since I guess this represents the bounds of the textfield, this is directly related to the truncated text.
You might have noticed that the new image has a more world options button. Ignore it.
Given this information, how can I make the textfields immediately look like the third image when resizing, rather than the second/fourth, so I fix this issue?
Is there any way to fix this bug while preserving the look of the text fields?
By my investigation the observed issue is originated from PlainTextFieldStyle style usage... no workaround for this (you can report feedback to Apple cause it is definitely issue). So if the topic issue is critical I recommend to use SquareBorderTextFieldStyle explicitly as below for text fields with such behaviour (at least temporary). Square style tested & worked in the posted scenario with Xcode 11.2 / macOS 10.15.
Here is result of different configurations:
Here is code for screenshot result:
GeometryReader { geometry in
VStack {
TextField("World Name", text: self.$WorldName)
.font(.system(size: geometry.size.width/24))
.textFieldStyle(SquareBorderTextFieldStyle())
.padding([.leading, .trailing], 6)
.frame(width: geometry.size.width*0.75, height: geometry.size.width/20)
// .background(
// RoundedRectangle(cornerRadius: 8)
// .fill(Color.init(white: 0.28))
// )
TextField("World Seed", text: self.$WorldSeed)
.font(.system(size: geometry.size.width/24))
.textFieldStyle(PlainTextFieldStyle())
.padding([.leading, .trailing], 6)
.frame(width: geometry.size.width*0.75, height: geometry.size.width/20)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.init(white: 0.28))
)
}
}