SwiftUI TextField wobble on text change. Doesn't reproduce on all machine - swift

So I made a simple app with simple text field instance bound to a local variable in SwiftUI
TextField("Main task", text: $store.mainTask)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(Font.custom("SF Pro Display", size: 14))
The text field is wrapped inside a view with transition and animation.
// text field is inside custom View
// conditional rendering—if that matters
if (true) {
CustomView()
.transition(.asymmetric(insertion: AnyTransition.opacity.animation(Animation.easeInOut(duration: 1).delay(0.5)), removal: AnyTransition.opacity.animation(Animation.easeInOut(duration: 0.1))))
}
I distributed the beta. Some people say they experienced the text wobbling as they typed. Some didn't. Does anyone know what's the reason?
I think it might be related with the animation, but I'm not so sure.

Related

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

How do I change text color when navigation link is selected? SwiftUI Mac Catalyst

I have used a List of NavigationLink to generate my sidebar but when running as a Mac Catalyst app all selections use the system accent color as their background. This is fine but where the colour is dark I want the text to invert to white like it does in most Mac apps.
Can anyone help? Here's my code:
List {
ForEach(topics, id: \.self) { topic in
NavigationLink(
destination: DetailView(selectedDate: self.titles[topic])
) {
HStack {
Image(topic)
Text(self.titles[topic]!)
}
}
}
}
Btw topic is an array of strings which are the keys for the dictionary titles. Thanks in advance.
Have you tried using the new Label(_:) element, instead of a custom HStack with image + text? Swift "2" now has Label which when coupled with the SidebarListStyle should do what you want to. Text goes to highlighted color when selected. Since you're using custom images, I'm not sure what happens there, but give it a try.
Instead of:
HStack {
Image(...)
Text(...)
}
Just use:
Label("Formalism", image: "formalism.png")

How does this SwiftUI binding + state example manage to work without re-invoking body?

A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
#Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text binding, which would in turn mutate the text #State var. This would then invalidate the ContentView, triggering a fresh invocation of body. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body only gets hit once, while WrappedText's body gets run every time the binding changes. And yet, as far as I can tell, the text state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.

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