How to check if item is visible - SwiftUI ScrollView - ios

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

Related

SwiftUI ScrollView w/LazyVStack calls onAppear of new elements as soon as they're added

I have a scroll view with a view hierarchy as below:
ScrollView {
CustomHeaderView()
LazyVStack {
VStack {
ForEach(someArray.indices, id: \.self) { index in
VStack {
HStack {
VStack {
someCustomView()
}
}
}
}
}
}
}
I'm attempting to add pagination to this view by adding onAppear block. The onAppear blocks fire as I'd expect, EXCEPT for when users scroll very fast. It appears that once the LazyVStack adds additional subviews, the scrollview doesn't retain it's position, instead keeps scrolling to the bottom as views are added.
I'd like to be able to add subviews to the LazyVStack but have the scrollview maintain its position, so that pagination events aren't triggered endlessly as the scroll view reloads.

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

SwiftUI ScrollView scrolls too far when using scrollViewReader

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.

Animate removing/adding SwiftUI view while animating offset

I'm trying to animate the offset of a SwiftUI view, while at the same time fading out and removing a subview of that view. The problem I'm running into is that SwiftUI performs the offset and fade-out animations, but doesn't combine them.
What I want to achieve to animate the position of the whole SubView, while simultaneously fading out the subtitle text, so that the subtitle text moves vertically while fading in or out. I can achieve this by animating the opacity of the Text instead of removing it, but that means the text will still take up "layout space".
Is there a way to achieve this animation with the if showSubtitle statement?
The following code and GIF demonstrate the problem:
struct ContentView: View {
#State private var showSubtitle = true
var body: some View {
SubView(showSubtitle: showSubtitle)
.animation(.default)
.offset(y: showSubtitle ? 100 : 0)
.onTapGesture {
self.showSubtitle.toggle()
}
}
}
struct SubView: View {
let showSubtitle: Bool
var body: some View {
VStack {
Text("Header")
if showSubtitle {
Text("Subtitle")
}
}
}
}
Actually the observed behaviour is because .offset does not change layout, the view is stand at the same place. So when you remove subview it is removed in-place and animating that removal (with default .opacity transition). The part that starts offsetting does not contain already subview, so you don't see it in moving up part.
Here is something that might give some kind of effect you expect, but transitions are based on source size, so it is not so far and manually specified distance of offset. Anyway, try:
if showSubtitle {
Text("Subtitle")
.transition(AnyTransition.opacity.combined(with: AnyTransition.move(edge: .top)))
}
Tested with Xcode 12 / iOS 14

Change background color of View inside TabView having NavigationView and ScrollView in SwiftUI

I want to build a TabView with 4 tabs having collection views in it. Below is my code of one tab named 'Gallery'.
var body: some View {
NavigationView {
ScrollView {
GridStack(rows: 3, columns: 2) { row, column, totalColumn in
CardView(card: self.cards[(row * totalColumn) + column])
}.padding().background(Color.red)
}
.navigationBarTitle("Gallery")
}
}
When I give background color for ScrollView, scrolling is not working for NavigationView largeTitle. How can I achieve this, I want to give red color for full view's background? What if I need to achieve this same backgorund color for all tabs?
Here is possible approach (scroll view is not broken in such case)
NavigationView {
GeometryReader { gp in
ScrollView {
ZStack(alignment: .top) {
Rectangle().fill(Color.red) // << background
// ... your content here, internal alignment might be needed
}.frame(minHeight: gp.size.height)
}
.navigationBarTitle("Gallery")
}
}

Resources