'SwiftUI - ScrollView behaves different based on viewport size
I have a simple SwiftUI ScrollView. I need to have the row(cell) closest to center of the viewport move itself to exact center (same distance from top and bottom) after scrolling has finished. Using the code below this works fine but only when row height is exactly the same as viewport height. If row heigh is different than viewport height the row does scroll itself to next closest row but it doesn't stop there and keeps scrolling to first row. By viewport I mean the actual visible scroll area. here is the code I have so far (taken by bits and pieces from all around here).
import SwiftUI
import Combine
struct SecondTry: View {
let detector: CurrentValueSubject<CGFloat, Never>
let publisher: AnyPublisher<CGFloat, Never>
init() {
let detector = CurrentValueSubject<CGFloat, Never>(0)
self.publisher = detector
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.detector = detector
}
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.gray)
.frame(height: 100)
.padding()
GeometryReader { geo in
ScrollViewReader { reader in
ScrollView {
VStack(spacing: 0) {
ForEach(0...100, id: \.self) { i in
Rectangle()
.frame(width: 200, height: 70)
.foregroundColor(.green)
.overlay(Text("\(i)"))
}
}
.frame(maxWidth: .infinity)
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) { detector.send($0) }
}.coordinateSpace(name: "scroll")
.onReceive(publisher) {
var index = $0/70
// Check if next item is near
let value = index < 1 ? index : index.truncatingRemainder(dividingBy: CGFloat(Int(index)))
print("index: \(index) $0: \($0) geo.size.height: \(geo.size.height) value: \(value)")
if value > 0.5 { index += 1 }
else { index = CGFloat(Int(index)) }
//index = index.rounded() //this does the same as above if - else
print("final index: \(index)")
// Scroll to index
withAnimation { reader.scrollTo(Int(index), anchor: .center) }
}
}
}
.frame(height: 210) //if this is 70 too like row height then it works fine
}
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
When I replaced my onReceive with this now it works fine but I feel I shouldn't be using if else to stop it from scrolling forever.
.onReceive(publisher) {
var index = ($0/70) + 2
var indexRounded = 0
indexRounded = Int(index.rounded())
print("index: \(index)")
print("indexRounded: \(indexRounded)")
// Scroll to index only if index is not equal to indexRounded
if CGFloat(indexRounded) == index {
print("nothing")
} else {
withAnimation { reader.scrollTo(Int(index), anchor: .center) }
}
}
Why is the behavior different based on row height vs. viewport height (visible scrollview height)?
I basically want it to behave like iOS 15 DatePicker/TimePicker where after scrolling row centers itself in the middle.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
