SwiftUI: ScrollViewReader 配合 ScrollView 实现精准定位

1,060 阅读2分钟

ScrollViewReader是SwiftUI中的一个视图容器,它可以协调多个滚动视图的滚动位置。

再开始今天的主题之前,我们先用ScrollerView来做一个简单的例子。

struct ScrollerViewReaderSample: View {
    var body: some View {
        ScrollView {
            ForEach(0..<50) { i in
                Text("This is #(i)")
                    .frame(height: 180)
                    .frame(maxWidth: .infinity)
                    .background(Color.white)
                    .cornerRadius(10)
                    .shadow(radius: 10)
                    .padding()
            }
        }
    }
}

image.png

循环了50个Text的 ScrollerView,此时如果你想要定位到第30个Text的位置,你该如何做? 你如果只使用ScrollerView肯定是很难做到的,此时你只需要和ScrollViewReader配合使用,你讲轻松拿捏这个需求。

下面具体来看看。

我们添加如下代码把Text的大列表包裹起来

ScrollView {
            ScrollViewReader { proxy in
                
                Button("Scroll to 30") {
                    proxy.scrollTo(30)
                }
                
                ForEach(0..<50) { i in
                    Text("This is #(i)")
                        .frame(height: 180)
                        .frame(maxWidth: .infinity)
                        .background()
                        .cornerRadius(10)
                        .shadow(radius: 10)
                        .padding()
                }
            }
        }

image.png

ScrollViewProxy 用于控制一个ScrollView的滚动 位置和行为。

当你点击Button,让ScrollView滚动到第30个Text的位置时,他不会变化。原因是因为你们有把Text和proxy建立关系,当你指定要滚动到第30个Text时,它不知道在哪里。

所以我们需要建立联系,我们需要给Text设置一个id。

Text("This is #(i)")
                        .frame(height: 180)
                        .frame(maxWidth: .infinity)
                        .background()
                        .cornerRadius(10)
                        .shadow(radius: 10)
                        .padding()
 .id(i) // id的值是foreach的索引值 i

这时当你点击按钮,就可以跳转到指定的位置了

跳转方法有两个参数,一个参数是位置,另一个是锚点位置。定义如下:

public func scrollTo<ID>(_ 
id: ID, 
anchor: UnitPoint? = nil // 指名跳转停留的位置
) where ID : Hashable
proxy.scrollTo(30, anchor: .bottom) // 停留在第三十个Text的 底部
proxy.scrollTo(30, anchor: .top) // 停留在第三十个Text的 顶部

使用proxy.scrollTo方法ScrollerView滚动是没有动画的。如果你想要有动

效果,可以自己在调用的地方加上动画效果。

Button("Scroll to 30") {
  withAnimation(.spring()) {
    proxy.scrollTo(30, anchor: .bottom)
  }
}

如果我们不想嵌套在ScrollerView里面,改如何实现代码呢?比如我们输入一个数字,就让它跳转到对应的位置?

那么下面我们来改造一下,增加一个TextField,当你输入一个数字,然后 点击Button,就跳转到输入的数字的位置。

struct ScrollViewReaderSample: View {
    @State var textFiledText: String = ""
    @State var scrollToIndex: Int = 0
    
    var body: some View {
        VStack {
            VStack {
                
                TextField("Input a number", text: $textFiledText)
                    .font(.largeTitle)
                    .keyboardType(.numberPad)
                    .frame(height: 55)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .padding(.horizontal)
                    .background(Color.green)
                
                Text("Click me".uppercased())
                    .font(.headline)
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.black.cornerRadius(10))
                    .onTapGesture {
                        if let index = Int(textFiledText) {
                            scrollToIndex = index
                        }
                    }
            }
            
            ScrollView {
                ScrollViewReader { proxy in
                    ForEach(0..<50) { i in
                        Text("This is #(i)")
                            .frame(height: 180)
                            .frame(maxWidth: .infinity)
                            .background()
                            .cornerRadius(10)
                            .shadow(radius: 10)
                            .padding()
                            .id(i)
                    }
                    .onChange(of: scrollToIndex) { newValue in
                        withAnimation(.spring()) {
                            proxy.scrollTo(newValue)
                        }
                    }
                }
            }
        }
    }
}

image.png

当我们点击按钮,就可以跳转到输入框里面输入的数字的位置。我们主要使用了onChange这个方法,那么官方的定义是当指定值更改时执行操作。在我们的例子中, 也就是当scrollToIndex 值改变,就会调用后面的方法。官方解释如下:

image.png

ScrollViewReader另一个很好的场景就是聊天界面,当有新的消息来了,就滚动到最新消息的视图位置,我们使用数组来关联,指定跳转到数组的大小位置即可。

大家有什么看法呢?欢迎留言讨论。

公众号:RobotPBQ