ScrollViewReader .scrollTo method doesn't allow padding when scrolling to TextField - ios

I'm using Xcode 13.2 and building for iOS 15.0 + with SwiftUI 3.0 and Swift 5
Here is my code:
ScrollViewReader { value in
VStack {
List(0..<things.count, id: \.self) { index in
VStack {
HStack {
Image(systemName: "circle")
TextField(
"Text Field",
text: $things[index]
)
.focused($focusField, equals: index)
.submitLabel(.next)
.onSubmit {
funcThatScrollsToLastInput()
}
}
}
.padding(.bottom, 20)
.id(index)
}
}
}
I have a ScrollView that on the return key, goes to the next TextField, which is great!
However, I cannot seem to add padding to the bottom of the TextField, nor to its wrapping HStack, VStack, or even if I add a rectangle, from within the confines of the stack that has the .id(index) modifier.
I want the TextField, when scrolled to, to have a padding(.bottom) of 10 or 20, but right now it's a 0.
It always seems to scroll exactly to the bottom the TextField originally had, or in other words without any style modifications.

You are confusing padding for the amount of scrolling that has occurred as a result of your changing the .focus(). Your list rows have 20 padding on them. That is just the visual space around the view using other views away.
The issue you are having is that when you programmatically change the .focus() on a TextField in a List, the TextField is scrolled to high above the keyboard. You could have 0 or 100 padding, and that behavior would not change.
The fix is to finish implementing the ScrollViewReader and add a .scrollTo() in your code. Since you did not post a Minimal, Reproducible Example (MRE), I just simply coded it in the .onSubmit()
.onSubmit {
// I made focusField an optional, so I had to safely unwrap. If your implementation is
// non-optional, then you don't need this.
guard let focusField = focusField else {
return
}
if focusField < things.count - 1 {
self.focusField = focusField + 1
// The DispatchQueue.main.asyncAfter(deadline:) is necessary because the .scrollTo()
// has to wait for the .focus() to finish its scroll
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
// If your implementation of focusField is non-optional, then change this to
// focusField + 1 as the one below is a local constant, so the implementation
// uses focusField + 2 as you want the cell below the current cell to be in view/
value.scrollTo(focusField + 2, anchor: .bottom)
}
}
}

Related

TextField(_:text:axis:) breaks keyboard avoidance inside ScrollView in iOS 16

Automatic keyboard avoidance seems to work fine if it's a regular TextField (i.e. one that doesn't expand on an axis), whether or not it is contained in a ScrollView
Keyboard avoidance also seems to work with the new TextField(_:text:axis) introduced in iOS 16 if it's simply placed in a VStack without being wrapped in a ScrollView. It will even continue to avoid the keyboard correctly as the height expands with more text.
But I can't seem to get keyboard avoidance to work with TextField(_:text:axis) if it is placed inside a ScrollView
I can employ the hacky method of using a ScrollViewReader combined with DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) to wrap the proxy.scrollTo() when the TextField is focused. This sort of works when you first focus the field, but I can't seem to get the ScrollView to continue to adjust its position as the TextField expands.
Here is an example:
struct KeyboardAvoidingView: View {
#State var text = ""
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
Color.red
.frame(height: 400)
Color.blue
.frame(height: 400)
TextField("Name", text: $text, axis: .vertical)
.padding(.vertical)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) {
withAnimation(.default) {
proxy.scrollTo(0)
}
}
}
.onChange(of: text) { newValue in
proxy.scrollTo(0) // This doesn't seem to do anything
}
Spacer()
.frame(height: 0)
.id(0)
}
}
}
}
}
I guess I'm wondering whether this is expected behavior, or a bug. And regardless if it's one or the other, I'm wondering if I can have an auto-expanding text field inside a scroll view that I can make avoid the keyboard even as the height of the field expands?
UPDATE: It turns out, the issue was with placing the TextField inside a VStack instead of a LazyVStack. I assume ScrollView doesn't know what to do with just a regular VStack in certain situations. If I replace the VStack with a LazyVStack in my example, everything works as expected!
I answered the question with the update posted above. The issue was with using VStack instead of LazyVStack
This is a long time known bug in the TextField component, but you may achieve the desired behavior by using an anchor: .bottom in the proxy.scrollTo call of your onChange.
it'll look like this:
// ...
TextField("Name", text: $text, axis: .vertical)
.padding(.vertical)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) {
withAnimation(.default) {
proxy.scrollTo(0)
}
}
}
.onChange(of: text) { newValue in
// This will always scroll to the bottom of the text editor,
// just make sure to pass the right value in the first parameter
// that will identify your TextEditor
proxy.scrollTo(MyTextEditorId, anchor: .bottom)
}
// ...
You may need some additional work to handle the editing of upper parts of the text editor when it's taller than your screen

SwiftUI - Text elements not starting from same leading position

I am still a beginner in swiftUI.
Adding code and screenshot which will have explain my problem better. In the below screenshot, how can I align 'E' of first line with 'T' of 2nd line. I want both 'Text' elements to have same leading space i.e. both should start from the same position from left.
I have seen cases where people are using just one Text element and '\n' to move text to the next line and that aligns the text, but in my case I will be having more elements such as TextField and some more Text elements below these 2 texts hence I can't use the '\n' idea.
Code:
struct TestData: View {
var body: some View {
ZStack {
Image("backgroundImage").resizable().edgesIgnoringSafeArea(.all).scaledToFill()
VStack(alignment: .leading, spacing: 10) {
Text("Enter your Data")
.font(.largeTitle).bold()
.frame(width: UIScreen.main.bounds.width-50, height: 33.0)
Text("This is a very very long text to wrap on the next line. This text is of 2 lines.")
.font(.callout)
.frame(width: UIScreen.main.bounds.width-50, height: 80.0)
.foregroundColor(.gray)
.lineLimit(nil)
HStack {
// Contains image and textfield. Data will be entered in textfield.
}
// Move Text and Button elements.
}.offset(x: -10, y: -100)
}
}
}
struct TestData_Previews: PreviewProvider {
static var previews: some View {
TestData()
}
}
Screenshot:
In general, don't set fixed frames; SwiftUI tends to work better when you let the layout engine do its thing. Using maxWidth/maxHeight and minWidth/minHeight can be useful for giving clues to the layout engine about what you want.
Similarly with offset - This moves things around but doesn't change their layout bounding box, so you can end up with overlapping elements (which is fine if that is what you want).
For your layout, you can simply remove the frame and offset and use some padding to shift everything in from the leading edge:
VStack(alignment: .leading, spacing: 10) {
Text("Enter your Data").font(.largeTitle)
.bold()
Text("This is a very very long text to wrap on the next line. This text is of 2 lines.").font(.callout)
.foregroundColor(.gray)
.lineLimit(nil)
HStack {
// Contains image and textfield. Data will be entered in textfield.
}
}.padding(.leading,50)

ScrollView touch works despite all other touch is disabled

So my goal is to disable touch events for everything on screen and I don't want to use .disable on every view or to use Color.black.opacity(0.00001) because of the code smell. I want it to be a block that isn't visible for the user like if I would overlay Color.clear over the whole view. And I want it to behave like if I were to use Color.black.opacity(0.1) with it disabling touch events on every view underneath.
If I for example use a ZStack with:
Color.black.opacity(0.2) every view underneath will no longer register touch events. (I want this, but it should be transparent)
Color.black.opacity(0) every view underneath will register touch events.
Color.black.opacity(0).contentShape(Rectangle()), some events will register, some won't, for example buttons won't work though scrolling in a ScrollView, or using a toggle will still work.
Here is some example code
struct ContentView: View {
#State var numberOfRows: Int = 10
var body: some View {
ZStack {
Color.white
ScrollView {
VStack {
ForEach(0..<numberOfRows, id: \.self) { (call: Int) in
Text(String(call))
.frame(maxWidth: .infinity)
}
}
}
Button(action: {
numberOfRows += 1
}) {
Color.blue
.frame(width: 300, height: 300)
}
Color.black.opacity(0) // <- change this to 0.x to disable all touch
.contentShape(Rectangle()) // <- Remove this line to make blue button work (opacity needs to be 0)
}
}
}
Why is scrollview still receiving touch events and why is buttons not?
Is there a way to make my touch events for every view underneath, disabled?
Use instead (tested with Xcode 13.4 / iOS 15.5)
Color.clear
// .ignoresSafeArea(.all) // << if need all screen space
.contentShape(Rectangle())
.highPriorityGesture(DragGesture(minimumDistance: 0))

Prevent overflow of Element in ZStack SwiftUI

I have an arrangement like this for a Horizontal Progress bar in SwiftUI
ZStack {
// UI Content
HStack{
Text("")
.frame (width:300)
}
}
The problem is text will move outside the ZStack when it is at the extreme right or left.
This is what I am trying to achieve (2nd image)
Any solution to achieve this?
I solved this solution using the alignment property. Idk whether this is the correct solution or a workaround.
First I made the Stack that contains the text to take the entire width of its parent.
Since the parent element was made using a ForEach, the text will be only displayed in some indices of List.
and I created a function like this
func textAlignment (currentIndex: int, totalIndex:Int) -> Alignment {
if (currentIndex == 0){
return Alignment.leading
}
if (currentIndex == totalIndex-1){
return Alignment.trailing
}
return Alignment.center
}
and I added this to Text stack using .frame(alignment: textAlignment ()) property.

SwiftUI alignment of "rating" views in two separate rows in Form for macOS and iOS

Writing a Multiplatform app initially for macOS and iOS that uses a master detail view structure and within the detail view is a Form and many Sections, one of which contains two structs that represent a user's rating (of food) for enjoyment and consumption.
Here is the code for the enjoyment rating...
struct Rating: View {
#Environment(\.colorScheme) var colourScheme
#Binding var rating: Int
var body: some View {
HStack {
Text("Enjoyment")
.padding(.trailing, 12.0)
#if os(iOS)
Spacer()
#endif
ForEach(0..<5) { counter in
Image(systemName: rating > counter ? "star.fill" : "star")
.onTapGesture(count: 1) {
if rating == 1 {
rating = 0
}
else {
rating = counter + 1
}
}
// this line for image system name = "circle.fill" : "circle"
//.padding(.trailing, counter != 4 ? 2.5 : 0)
}
.shadow(color: colourScheme == .light ? .gray : .white, radius: colourScheme == .light ? 1.0 : 2.0, x: 0, y: 0)
}
.foregroundColor(Color.accentColor)
}
}
The consumption rating is very similar with minor changes.
So far these look good on iOS because the (iOS only) Spacer() pushes each Rating view to the right or trailing edge of the row and my .padding modifier hack for the circle image makes the spacing between each image "about right".
While I'm struggling to figure out how to align the "dots" for the macOS target, I'm also struggling to figure out how to align each image programmatically, so that if I changed the image the alignment would work.
See screenshots below (that illustrate how the five Images do not align).
iOS
macOS
I've read a few blogs on the .alignmentGuide modifier including Alignment guides in SwiftUI by Majid Jabrayilov.
It seems to be the way I should go but I'm stuck in my attempts on how to work this out.
I've added a comment about the use of Spacer() between your label and your rating control, but separately it's worth looking at the rating control itself.
Firstly, right now you're relying on the five elements sharing an HStack with the label, and using conditional padding logic within the loop to control the spacing between elements.
That part would be easier if you give your rating element its own HStack. That way, spacing between elements can be determined using the stack's spacing attribute without having to worry about whether or not you're on the last loop iteration. For example:
HStack(spacing: 2.5) {
ForEach(0..<5) {
Image(systemName: "circle")
// etc.
}
}
In terms of aligning the child elements of a rating view so that they align with a similar view below regardless of the symbol being used, you can constrain the frame width of each child element to be the same, regardless of what image they're displaying.
You can accomplish that by adding a .frame() modifier to each child element in the loop:
HStack {
ForEach(0..<5) {
Image(systemName: "xxx")
.frame(width: 40, alignment: .center)
}
}
You'd obviously need to pick a width that works for you - and you could mix this with a spacing attribute on the HStack as well.

Resources