I'm trying to create a scrollview with a custom navigator in the bottom. The navigation item should get a background when the scrollview is inside it's proximity.
I've used a scrollviewReader to save the item and the yOffSet inside an array. Then I've given a YOffsetScrollValuePreferenceKey to the entire HStack inside the scrollview. Lastly I listen to if YOffsetScrollValuePreferenceKey value changes, and if it does compare the new value with the value of the items inside the array. If the value exists then I set the selected item to the item belonging to that offset.
My problem occurs when I change the orientation of the device. If a user has scrolled let's say for example to the middle of the list, the position of the items will be calculated from that position. This means that instead of the first item having a yOffSet of 0, it now has a negative number (based on how far the user has scrolled). I need the items yOffSet to be calculated based on their position inside the scrollview, not based on where the user is inside the scrollview. Is there a way to do this?
I've already tried to let the scrollview scroll back to the first item upon change of orientation. This solution did not work, as the position of the item changed when the orientation changed, and this gave some other buggy behaviour. I've ran all out of ideas so hoped someone could help me here. :)
I will provide simple code where the problem is isolated below! If you have any more questions or need me to provide more information please let me know. To run into the problem run the code, scroll the list to the middle (or anywhere else apart from the starting position) change the orientation of the device, and scroll to a different section. The navigation view under the scrollview now does not run in synch with which view is on screen.
import SwiftUI
struct ContentView: View {
@State private var numberPreferenceKeys = [NumberPreferenceKey]()
@State var selectedNumber = 0
@State var rectangleHeight: [CGFloat] = [
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000)
]
let colors: [Color] = [Color.blue, Color.red, Color.green, Color.gray, Color.purple]
var body: some View {
VStack {
ScrollViewReader { reader in
ScrollView(.horizontal) {
HStack {
ForEach(0..<5) { number in
Rectangle()
.fill(colors[number])
.frame(width: rectangleHeight[number], height: 200)
.id("\(number)")
.background(
GeometryReader { proxy in
if numberPreferenceKeys.count < 6{
var yOffSet = proxy.frame(in: .named("number")).minX
let _ = DispatchQueue.main.async {
var yPositiveOffset: CGFloat = 0
if number == 1, yOffSet < 0 {
yPositiveOffset = abs(yOffSet)
}
numberPreferenceKeys.append(
NumberPreferenceKey(
number: number,
yOffset: yOffSet + yPositiveOffset
)
)
}
}
Color.clear
}
)
}
}
.background(GeometryReader {
Color.clear.preference(
key: YOffsetScrollValuePreferenceKey.self,
value: -$0.frame(in: .named("number")).origin.x
)
})
.onPreferenceChange(YOffsetScrollValuePreferenceKey.self) { viewYOffsetKey in
DispatchQueue.main.async {
for numberPreferenceKey in numberPreferenceKeys where numberPreferenceKey.yOffset <= viewYOffsetKey {
selectedNumber = numberPreferenceKey.number
}
}
}
}
HStack {
ForEach(0..<5) { number in
ZStack {
if number == selectedNumber {
Rectangle()
.frame(width: 30, height: 30)
}
Rectangle()
.fill(colors[number])
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
reader.scrollTo("\(number)")
}
}
}
}
}
}
.coordinateSpace(name: "number")
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
numberPreferenceKeys = []
}
}
}
struct NumberPreferenceKey {
let number: Int
let yOffset: CGFloat
}
struct YOffsetScrollValuePreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}