SwiftUI ScrollView scrolls too far when using scrollViewReader - ios

I'm creating an app where I'm presenting data in paged views and each view contains a vertical scrollable list, enabled by scrollView. I'm also using scrollViewReader to be able to programmatically scroll to a specific position in the list. This works OK in most cases, but when I programmatically scroll to the bottom of the list (to the last item), it scrolls too far.
I have isolated a case where this is repeatable:
ScrollView(.vertical, showsIndicators: false) {
ScrollViewReader { scrollView in
LazyVStack(alignment: .leading) {
ForEach((1..<100) , id:\.self) { key in
Text("Hello \(key)")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation {
scrollView.scrollTo(99, anchor: .center)
}
}
}
}
}
If you run this on e.g. the iPhone 12 simulator, you will se that the last item in the list ends up in the middle of the screen. If you manually scroll and release, the list will "fall down" to the bottom of the screen, which is the position I want the list to have even though I scroll to the last item with .anchor: .center.
Is there some way around this behavior?
Edit: Realized I wasn't clear enough. I want the anchor to be .center as long as I have more scrollable content in my view. But when I get to the bottom of the list I want the scrollView to be smart enough to know that it's not possible to scroll any longer. As it is now, it scrolls more than is possible if you scroll manually.

Related

How to align and anchor SwiftUI horizontal list at right edge of view?

I am looking for how to align items in a list horizontally, where the items start at the last item, or on the right edge, and go left from there.
For example, with a list of 1-20, I'd be looking to show items 1-5 off the left edge of the screen, with item 20 being the right-most item, right on the edge of the screen. An image of this can be shown below (it is not the solution running, but rather a contrived image to show the desired effect).
How can I do this in SwiftUI?
An HStack will simply center itself horizontally. HStack doesn't seem to have a horizontal alignment that I can control.
An HStack in a scrollview allows me to reach the right-most item, but still starts at the first item, or the left-most edge of the list.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Divider()
// the HStack just tries to fit everything within the width of the view,
// without anything going "offscreen"
HStack(spacing: 10) {
ForEach(1..<21) { index in
Text("\(index)")
}
Spacer()
}
Divider()
// placing the HStack within a ScrollView does allow elements to go offscreen,
// but the list starts at 1 and goes offscreen at 16 on the right
// I am looking for how to "anchor" the list on the right edge instead,
// with elements 1 through 5 being displayed off the left edge of the screen,
// and the right-most element being 20, directly on the right edge of the screen
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(1..<21) { index in
Text("\(index)")
}
}
}
.frame(height: 100)
Divider()
// are there any other ways to achieve my desired effect?
}
}
}
If I properly understand question looks like you need ScrollViewReader:
// Define array for your label
var entries = Array(0...20)
ScrollView(.horizontal) {
ScrollViewReader { reader in
HStack(spacing: 10) {
ForEach(entries, id: \.self) { index in
Text("\(index)")
}.onAppear {
// Here on appear scroll go to last element
reader.scrollTo(entries.count - 1)
}
}
}
}

SwiftUI InputAccessoryView

I'm trying to build a chat view in SwiftUI and I want to append my input views to the keyboard, so that when I dismiss the keyboard by dragging my view gets moved with it.
When I was using UIKit I overwrote the inputAccessoryView of the ViewController. Is something similar possible with SwiftUI?
EDIT:
I already saw that I can add a UIKit TextField and add a InputAccessory for this text field. However that's not what I want to do. I want to have a global inputAccessoryView in my SwiftUI View and add my custom input view as a subview, so that it is always Visible and not an addition to my TextField.
I see two possible solutions to the behavior you want.
In some cases, SwiftUI views move out of the way of the keyboard automatically
in iOS 15 and later you can create an InputAccessoryView in Swiftui
1: In swiftUI, there are several safe areas which views lay themselves inside of by default. One of these is the keyboard safe area. This areas takes up the full screen of the device when the keyboard is hidden but shrinks to the non keyboard area of the screen when the keyboard is displayed. So in the example code below, the text field should move above the keyboard when it appears and drop down when the keyboard disappears (this does not work on an iPad when the keyboard is in the smaller floating mode).
VStack {
ScrollView {
ForEach(0 ..< 50) { item in
Text("Demo Text")
.frame(maxWidth: .infinity)
}
}
TextField("Enter Text", text: $messageText)
}
2: In iOS 15+, you can create a toolbar in the keyboard location. This essentially acts as an InputAccessoryView does in UIKit. The difference between this and method 1 is that a view in here will only appear when the keyboard is displayed. The one expiation to this is when a wired or wireless keyboard is attached to the iPhone or iPad, the toolbar view will still be displayed just at the bottom of the screen.
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Text("Apears at top of keyboard")
}
}
So putting 1 and 2 together, here is an example that implements both. You can run it in Xcode to help understand how both methods behave
VStack {
ScrollView {
ForEach(0 ..< 50) { item in
Text("Demo Text")
.frame(maxWidth: .infinity)
}
}
TextField("Enter Text", text: $messageText)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Text("Apears at top of keyboard")
}
}

How to check if item is visible - SwiftUI ScrollView

Trying to programmatically determine when an item is being displayed on screen in a ScrollView in SwiftUI. I understand that a ScrollView renders at one time rather than rendering as items appear (like in List), but I am constrained to using ScrollView as I have .scrollTo actions.
I also understand that in UIKit with UIScrollView, it is possible to use a CGRectIntersectsRect between the item frame and the ScrollView frame in the UIScrollViewDelegate but I would prefer to find a solution in SwiftUI if possible.
Example code looks like this:
ScrollView {
ScrollViewReader { action in
ZStack {
VStack {
ForEach(//array of chats) { chat in
//chat display bubble
.onAppear(perform: {chatsOnScreen.append(chat)})
}.onReceive(interactionHandler.$activeChat, perform: { _ in
//scroll to active chat
})
}
}
}
}
Ideally, when a user scrolls, it would check which items are on screen and zoom the view to fit the largest item on screen.
When you use VStack in ScrollView all content is created at once at build time, so onAppear does not fit your intention, instead you should use LazyVStack, which will create each subview only before it appears on screen, so onAppear will be called when you expect it.
So it should be like
ScrollViewReader { action in
ScrollView {
LazyVStack { // << this !!
ForEach(//array of chats) { chat in
//chat display bubble
.onAppear(perform: {chatsOnScreen.append(chat)})
}.onReceive(interactionHandler.$activeChat, perform: { _ in
//scroll to active chat
})
}
}
}

How can I know if I have scrolled past a certain child of ScrollView?

In SwiftUI, how can I know if I a child of a ScrollView is no longer visible because it has been scrolled out of the visible area?
For example,
ScrollView {
Text("A")
Text("B")
// ..
Text("Z")
}
How can I know that "A" is no longer visible?

Actions not working on Lists inside horizontal scrollviews in SwiftUI

I'm trying to make a horizontal scrollview that has multiple lists embedded inside of it, thing is that even implementing the delete method i can scroll the lists or the scrollviews but i can't use the delete action.
I wonder if is there any way i can overcome this problem, maybe by making the scrollview non-scrollable when the finger is on the cell or if there's a smarter option.
Here's the code:
var body: some View {
ScrollView(.horizontal, showsIndicators: true){
HStack(alignment: .top, spacing: 60){
ForEach(data.trips[0].days){ day in
List{
Section{
Text(String((day as Day).date))
ForEach(day.stops){ stop in
Text((stop as Stop).title)
}.onDelete(perform: self.delete)
}
}
.listStyle(GroupedListStyle())
}
}.frame(width:800)
}.background(Color.clear)
}
func delete(at offsets: IndexSet){
print(offsets)
}
Of course it slides left/right through the scrollview and up/down through the list
You're using a bunch of built-in conveniences like ScrollView and List, and I don't think there's any way to override their behaviours selectively by making only part of the field scrollable.
And even if you did hack that together, I think it would be really unintuitive for the user. It would mix the UI swipe metaphors in a way that isn't obvious at all.
You'll have to either implement your own scroll widget for the HStack, or add a delete interface.

Resources