SwiftUI ScrollView Keyboard Avoidance - swiftui

First off. I know the SwiftUI ScrollView has limitations. I am hoping to get around that here.
My problem is I have a chat box...so a VStack with a ScrollView and a text input. I am able to get the text input to be keyboard adaptive...i.e. when you type it shifts up with the keyboard.
The issue is that the ScrollView loses its scroll position. So, if you scroll to the bottom and then activate the keyboard...you can't see the bottom anymore. It's almost like a ZStack.
What I want is to mimic the same behavior as WhatsApp which maintains scroll position while typing a new message.
I know about the ScrollViewReader and scrollTo...that seems to be a hack in this case and I'm hoping to avoid it. If anything, maybe there is a layout related fix?

By a stroke of luck I was able to solve this by adding .keyboardAdaptive() to both the ScrollView and the text input as well as changing from padding to offset
struct KeyboardAdaptive: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
#State var offset: CGFloat
func body(content: Content) -> some View {
content
.offset(y: -keyboardHeight)
.onReceive(Publishers.keyboardHeight) {
self.keyboardHeight = $0 == 0 ? 0 : $0 - offset
}
}
}

Maybe a better option.
If you are using iOS 14+ with scrollview or have the option to use scrollview.
https://developer.apple.com/documentation/swiftui/scrollviewproxy
https://developer.apple.com/documentation/swiftui/scrollviewreader
Below might help
ScrollViewReader { (proxy: ScrollViewProxy) in
ScrollView {
view1().frame(height: 200)
view2().frame(height: 200)
view3() <-----this has textfields
.onTapGesture {
proxy.scrollTo(1, anchor: .center)
}
.id(1)
view4() <-----this has text editor
.onTapGesture {
proxy.scrollTo(2, anchor: .center)
}
.id(2)
view5().frame(height: 200)
view6().frame(height: 200)
submtButton().frame(height: 200)
}
}
imp part from above is
anyView().onTapGesture {
proxy.scrollTo(_ID, anchor: .center)
}.id(_ID)
Hope this helps someone :)

Related

SwiftUI offset items in ScrollView under another view

I have a VStack with some content at the top of my app, then a ScrollView on the bottom, with these views being seperated with a Divider. Is there any way to offset the scrollView such that it starts slightly tucked under the Divider and the top view?
Here is an example image of what I want:
The numbers are in a ScrollView and the top content is simply Color.white in this example.
If I apply a simply y offset, though, I get this:
The number is vertically shifted up, but not "tucked" under.
Is there an easy way to get the "tucked" result? I'm sure I could use a ZStack or something, but that seems like a lot of work, especially because I don't know how large the top content will be.
Example Code:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.white.frame(height: 100)
Divider()
ScrollView {
ForEach(0..<20) { number in
Text("\(number)")
}
}
.offset(y: -8)
}
}
}
I assume you just need padding for scroll view, like
ScrollView {
ForEach(0..<20) { number in
Text("\(number)")
}
}
.padding(.top, -8) // << here !!
.clipped()

ScrollView shows smudge at the bottom of the frame when scrolling the items

I am using the GridItems inside the ScrollView to show ten circles horizontally. It works fine but there are lines of smudge shown at the bottom border line of ScrollView container. I tried,
Addind padding to ScrollView.
Adding padding to HStack.
Any recommendations would be appreciated! I captured the screenshot scrolling all the way to the left and beyond holding the press.
Using iPhone 13 mini simulator,
here is the screenshot
struct HomeView: View {
var body: some View {
ScrollView.init([.vertical]) {
Section(header: Text("Today's Items").font(.system(size: 20.0, weight:.semibold, design: .rounded))) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(0..<20) { _ in
Circle()
.fill(Color.random)
.frame(width: 100, height: 100)
}
}.padding(.vertical).background(.blue)
}.padding(.vertical).background(.orange)
}
...
}
}
}
UPDATE: With help from Yrb, I verified that I only see this in iPhone 12 and 13 mini simulators only (with 13 device being worse). But I don't own either model physically. If anybody can test it on the physical devices and share the observation I would appreciate it.
Add padding to HStack, not to ScrollView

SwiftUI DragGesture() get start location on screen

I want to make a view "swipeable", however this view only covers a small share of the whole screen. Doing:
var body: some View {
ZStack {
Text("ABC")
.gesture(DragGesture().onChanged(onChanged))
}
}
func onChanged(value: DragGesture.Value) {
print(value.startLocation)
print("onChanged", value.location)
}
Gives me relative values where the user has swiped - however, without knowing the exact location of the view, I'm unable to know where the user's touch is on the screen.
In other words, when the Text is in the center of the screen, going all the way to the right edge gives 200px. But let's say the Text moved to the left, then it would be almost 400. I tried putting another gesture on the ZStack to save the initial value, however it appears you can't have multiple gestures within each other.
How can I make the listener translate the values so the left edge is 0 and the right edge is displayWidth in px, regardless of where the Text is?
You can make use of coordinate spaces, and use that with DragGesture.
Code:
struct ContentView: View {
#State private var location: CGPoint = .zero
var body: some View {
VStack(spacing: 30) {
Text("ABC")
.background(Color.red)
.gesture(
DragGesture(coordinateSpace: .named("screen")).onChanged(onChanged)
)
Text("Location: (\(location.x), \(location.y))")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.coordinateSpace(name: "screen")
}
private func onChanged(value: DragGesture.Value) {
location = value.location
}
}

Getting VStack to shrink to minWidth when containing Text SwiftUI

I'm still somewhat new to SwiftUI and I'm getting a weird case that I don't fully understand. Basically, I have a VStack that contains some Text Views but also has a background View. Ideally, I'd like the background to grow in width as much as it needs to up to a point. I figure that is what the minWidth and maxWidth are for in .frame()
I started with this and it seems to be working:
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message")
}
.background(
Rectangle()
.foregroundColor(Color.blue)
.frame(minWidth: 0,
maxWidth: 270)
)
}
}
So far so good, but when I make the text big enough that it would need to wrap, this is what I get.
So it seems that by putting the frame around the background only makes the min/max affect that background View.
If I then try to put the frame around the VStack, I get this:
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message")
}
.frame(minWidth: 0,
maxWidth: 270)
.background(
Rectangle()
.foregroundColor(Color.blue)
)
}
}
Even though I don't think I have something pushing it out, it still pushes out the the full maxWidth.
I've also tried moving the frame to the Text but that gives the same result.
What is the correct way to get a VStack with background to only grow with its contents up to a maxWidth?
Thank you!
Well I'm dumb, literally right after posting I remembered something about how the order of the modifiers on a View matter.
I put the frame after the background and it worked.
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message abcdefghijklmnopqrstuvwxyz")
}
.background(
Rectangle()
.foregroundColor(Color.blue)
)
.frame(minWidth: 0,
maxWidth: 270)
}
}
I'll leave my question here just incase it somehow helps someone else someday.

Animated transitions and contents within ScrollView in SwiftUI

I'm not quite a SwiftUI veteran but I've shipped a couple of apps of moderate complexity. Still, I can't claim that I fully understand it and I'm hoping someone with deeper knowledge could shed some light on this issue:
I have some content that I want to toggle on and off, not unlike .sheet(), but I want more control over it. Here is some "reconstructed" code but it should be able capture the essence:
struct ContentView: View {
#State private var isShown = false
var body: some View {
GeometryReader { g in
VStack {
ZStack(alignment: .top) {
// This element "holds" the size
// while the content is hidden
Color.clear
// Content to be toggled
if self.isShown {
ScrollView {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.frame(width: g.size.width) // This is a "work-around"
} // ScrollView
.transition(.move(edge: .bottom))
.animation(.easeOut)
}
} // ZStack
// Button to show / hide the content
Button(action: {
self.isShown.toggle()
}) {
Text(self.isShown ? "Hide" : "Show")
}
} // VStack
} // GeometryReader
}
}
What it does is, it toggles on and off some content block (represented here by a Rectangle within a ScrollView). When that happens, the content view in transitioned by moving in from the bottom with some animation. The opposite happens when the button is tapped again.
This particular piece of code works as intended but only because of this line:
.frame(width: g.size.width) // This is a "work-around"
Which, in turn, requires an extra GeometryReader, otherwise, the width of the content is animated, producing an unwanted effect (another "fix" I've discovered is using the .fixedSize() modifier but, to produce reasonable effects, it requires content that assumes its own width like Text)
My question to the wise is: is it possible to nicely transition in content encapsulated within a ScrollView without using such "fixes"? Alternatively, is there a more elegant fix for that?
A quick addition to the question following #Asperi's answer: contents should remain animatable.
You are my only hope,
–Baglan
Here is a solution (updated body w/o GeometryReader). Tested with Xcode 11.4 / iOS 13.4
var body: some View {
VStack {
ZStack(alignment: .top) {
// This element "holds" the size
// while the content is hidden
Color.clear
// Content to be toggled
if self.isShown {
ScrollView {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.animation(nil) // << here !!
} // ScrollView
.transition(.move(edge: .bottom))
.animation(.easeOut)
}
} // ZStack
// Button to show / hide the content
Button(action: {
self.isShown.toggle()
}) {
Text(self.isShown ? "Hide" : "Show")
}
} // VStack
}