SwiftUI:如何根据用户当前滚动视图的滚动位置同步/更改进度条进度?

SwiftUI: How do I sync/change progress bar progress based on scrollview’s user current scroll position?

提问人:eugene_prg 提问时间:2/19/2021 更新时间:3/3/2023 访问量:874

问:

我在 ScrollView 中有水平进度条,当用户滚动时,我需要更改该进度条值。

有没有办法将一些值绑定到当前滚动位置?

SwiftUI 滚动视图

评论

2赞 lorem ipsum 2/19/2021
这回答了你的问题吗?SwiftUI界面 |从 ScrollView 获取当前滚动位置

答:

3赞 George 10/6/2021 #1

你可以用几秒来做到这一点。GeometryReader

我的方法:

  1. 获取集装箱的总高度ScrollView
  2. 获取内容的内在高度
  3. 求总可滚动高度的差值
  4. 获取滚动视图容器顶部与内容顶部之间的距离
  5. 将顶部之间的距离除以可滚动的总高度
  6. 使用 s 设置值PreferenceKeyproportion@State

法典:

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

评论

0赞 Iraklis Eleftheriadis 3/3/2023
您将如何使此代码可重用到许多视图中?
0赞 George 3/3/2023
@IraklisEleftheriadis 您可能只是将一些视图内容传递给视图,并使用它而不是VStack { ForEach ... }
0赞 Iraklis Eleftheriadis 3/3/2023
我最终为 VStack 和 ScrollView 创建了两个自定义修改器。我在修改器中添加了您作为@Binding提供的两个变量,以便我可以使用它们来更新主视图中的进度条。
0赞 Iraklis Eleftheriadis 3/3/2023 #2

如果你想使 George 的代码可重用,你可以通过创建 2 个修饰符并将它们应用于你想要动画的视图来实现。解决方案如下:

内容视图.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()
    }
    
}