I am currently using **Xcode Version 14.3.1** (14E300c).
The scrollview in the code has an array of objects that I am iterating through with, ForEach(). When the user scrolls, lets say to object 30(each object is numbered, for demonstration purposes), with an anchor value of 0.00135(you can think of this as the offset value in relation to said object), i want the user to be able to return to the exact location prior to deinitializing the view; exactly like, pretty much, every social media application.
I have attempted to do this by using ScrollViewReader.scrollTo(<id: , anchor:), but to no avail. It works perfectly with preview, but as soon as I use the simulator or a device(iPhone 14), it starts to resize my ScrollView Content height in random sequences.
Can someone please explain why this would be working perfectly in preview but not on devices; and how can I make it work for devices without this strange bug?
So the issue happens only when you reach near the end(bottom) of the scrollable content; and oddly enough, if you scroll near the beginning(top) of the scrollable content, it fixes itself.
**// VIEW THAT IS PROBABLY CAUSING THE ISSUE**
struct ScrollViewWithSavedPosition: View {
@ObservedObject var scrollViewSavedValue: ScrollViewSavedValue
@State private var isViewLoaded: Bool = false
let geoProxy: GeometryProxy
let maxScrollableHeight: CGFloat = 4809
var body: some View {
ScrollViewReader { scrollProxy in
ScrollView(showsIndicators: false) {
LazyVStack {
ForEach(0..<34) { poster in
ZStack {
Rectangle()
.fill(.black)
.frame(width: geoProxy.size.width * 0.4,
height: geoProxy.size.height * 0.2)
Text("\(poster)")
.foregroundColor(.orange)
}
}
}
.id("scrollPosition")
.background(
GeometryReader {
Color.orange
.preference(key: CGPointPK2.self,
value: $0.frame(in: .global).origin)
}
)
.onPreferenceChange(CGPointPK2.self) { scrollPosition in
DispatchQueue.main.async {
isViewLoaded = true
}
if isViewLoaded {
let offsetValue = (-1 * (scrollPosition.y - geoProxy.safeAreaInsets.top)) / maxScrollableHeight
scrollViewSavedValue.scrollOffsetValue = offsetValue
}
print(scrollViewSavedValue.scrollOffsetValue)
}
}
.onAppear {
scrollProxy.scrollTo("scrollPosition", anchor: UnitPoint(x: 0, y: scrollViewSavedValue.scrollOffsetValue))
}
}
.preferredColorScheme(.light)
}
}
struct CGPointPK2: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
}
class ScrollViewSavedValue: ObservableObject {
@Published var scrollOffsetValue: CGFloat = 0
@Published var selectedTab: Int = 1
}
struct TabBarView: View {
@ObservedObject var scrollViewSavedValue: ScrollViewSavedValue
let geoProxy: GeometryProxy
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.fill(.blue)
.frame(width: geoProxy.size.width, height: 80)
HStack(spacing: 100) {
Button {
DispatchQueue.main.async {
scrollViewSavedValue.selectedTab = 1
}
} label: {
ZStack {
Circle()
.fill(.black)
.frame(width: 60, height: 60)
Text("View 1")
.foregroundColor(.white)
.font(.system(size: 12))
.fontWeight(.bold)
}
}
Button {
DispatchQueue.main.async {
scrollViewSavedValue.selectedTab = 2
}
} label: {
ZStack {
Circle()
.fill(.black)
.frame(width: 60, height: 60)
Text("View 2")
.foregroundColor(.white)
.font(.system(size: 12))
.fontWeight(.bold)
}
}
}
}
}
.ignoresSafeArea()
}
}
struct PresentationView: View {
@StateObject private var scrollViewSavedValue = ScrollViewSavedValue()
var body: some View {
GeometryReader { geoProxy in
switch scrollViewSavedValue.selectedTab {
case 1:
ScrollViewWithSavedPosition(scrollViewSavedValue: scrollViewSavedValue, geoProxy: geoProxy)
default:
Text("View 2")
.foregroundColor(.black)
}
TabBarView(scrollViewSavedValue: scrollViewSavedValue, geoProxy: geoProxy)
}
}
}
Hello and welcome to Stack Overflow,
After playing around with your code for a while, I believe the problem lies with the fact that the scroll height is not constant which it makes it difficult to determine where you should scroll to. I initially thought through using mathematics I could figure it out, but after some testing I found the scroll height is affected not only by the top padding, but also the bottom padding and the size of the tab bar. So each phone will need to be tested individually.
A much simpler solution is when changing to view 2, not to remove the scroll view, which I have renamed to ScrollPosterView
. This can be done by using the opacity
and disable
modifiers. This ensures that the scroll view is still rendered but is not able to be seen or operated on by the user. Using this method, the scroll view's scroll position does not need to be saved. As this looks like to be a social media type app, the images do not need to be reloaded, saving on processing power.
This is done as so (where I have simplified the code):
struct ContentView: View {
@State var selectedTab: Int = 1
var body: some View {
VStack(spacing: 0) {
ZStack {
ScrollPosterView()
.opacity(selectedTab == 1 ? 1 : 0)
.disabled(selectedTab == 2)
ScrollView {
VStack {
Text("View 2")
.foregroundColor(.black)
Spacer()
}
}
.disabled(selectedTab == 1)
.opacity(selectedTab == 2 ? 1 : 0)
}
TabBarView(selectedTab: $selectedTab)
}
}
}
Where ScrollPosterView
is:
struct ScrollPosterView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 10) {
ForEach(0..<20) { poster in
ZStack {
Rectangle()
.fill(.black)
.frame(width: 100,
height: 100)
Text("\(poster)")
.foregroundColor(.orange)
}
}
}
.background(Color.orange)
}
.preferredColorScheme(.light)
}
}
And TabBarView
is:
struct TabBarView: View {
@Binding var selectedTab: Int
var body: some View {
ZStack {
HStack(spacing: 100) {
Button {
selectedTab = 1
} label: {
ZStack {
Circle()
.fill(.black)
.frame(width: 60, height: 60)
Text("View 1")
.foregroundColor(.white)
.font(.system(size: 12))
.fontWeight(.bold)
}
}
Button {
selectedTab = 2
} label: {
ZStack {
Circle()
.fill(.black)
.frame(width: 60, height: 60)
Text("View 2")
.foregroundColor(.white)
.font(.system(size: 12))
.fontWeight(.bold)
}
}
}
}
.frame(height: 70)
.frame(maxWidth: .infinity)
.background(Rectangle()
.foregroundColor(.blue)
.ignoresSafeArea())
}
}