Reputation: 1168
I have horizontal progress bar within ScrollView and I need to change that progress bar value, when user is scrolling.
Is there any way to bind some value to current scroll position?
Upvotes: 4
Views: 1234
Reputation: 109
If you want to make George's code reusable, you can do so by creating 2 modifiers and applying them to the view you want to have the animation. Here is the solution:
ContentView.swift:
struct ContentView: View {
@State private var scrollViewHeight: CGFloat = 0
@State private var proportion: CGFloat = 0
@State private var proportionName: String = "scroll"
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(0 ..< 100) { index in
Text("Item: \(index + 1)")
}
}
.frame(maxWidth: .infinity)
.modifier(
ScrollReadVStackModifier(
scrollViewHeight: $scrollViewHeight,
proportion: $proportion,
proportionName: proportionName
)
)
}
.modifier(
ScrollReadScrollViewModifier(
scrollViewHeight: $scrollViewHeight,
proportionName: proportionName
)
)
ProgressView(value: proportion, total: 1)
.padding(.horizontal)
}
}
}
ScrollViewAnimation.swift
struct ScrollReadVStackModifier: ViewModifier {
@Binding var scrollViewHeight: CGFloat
@Binding var proportion: CGFloat
var proportionName: String
func body(content: Content) -> some View {
content
.background(
GeometryReader { geo in
let scrollLength = geo.size.height - scrollViewHeight
let rawProportion = -geo.frame(in: .named(proportionName)).minY / scrollLength
let proportion = min(max(rawProportion, 0), 1)
Color.clear
.preference(
key: ScrollProportion.self,
value: proportion
)
.onPreferenceChange(ScrollProportion.self) { proportion in
self.proportion = proportion
}
}
)
}
}
struct ScrollReadScrollViewModifier: ViewModifier {
@Binding var scrollViewHeight: CGFloat
var proportionName: String
func body(content: Content) -> some View {
content
.background(
GeometryReader { geo in
Color.clear.onAppear {
scrollViewHeight = geo.size.height
}
}
)
.coordinateSpace(name: proportionName)
}
}
struct ScrollProportion: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Upvotes: 1
Reputation: 30341
You can do this with a few GeometryReader
s.
My method:
ScrollView
containerPreferenceKey
s to set the proportion
@State
valueCode:
struct ContentView: View {
@State private var scrollViewHeight: CGFloat = 0
@State private var proportion: CGFloat = 0
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(0 ..< 100) { index in
Text("Item: \(index + 1)")
}
}
.frame(maxWidth: .infinity)
.background(
GeometryReader { geo in
let scrollLength = geo.size.height - scrollViewHeight
let rawProportion = -geo.frame(in: .named("scroll")).minY / scrollLength
let proportion = min(max(rawProportion, 0), 1)
Color.clear
.preference(
key: ScrollProportion.self,
value: proportion
)
.onPreferenceChange(ScrollProportion.self) { proportion in
self.proportion = proportion
}
}
)
}
.background(
GeometryReader { geo in
Color.clear.onAppear {
scrollViewHeight = geo.size.height
}
}
)
.coordinateSpace(name: "scroll")
ProgressView(value: proportion, total: 1)
.padding(.horizontal)
}
}
}
struct ScrollProportion: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Result:
Upvotes: 3