iOS13:
iOS13中, 想让SwiftUI的ScrollView滚动起来比较困难, 下面的方法主要是是通过ListScrollingHelper, 获得ScrollView的instance, 通过获得的instance来操作ScrollView。
struct ContentView: View {
// proxy helper
@State var scrollingProxy = ListScrollingProxy()
// 重要,通过这个Bool的flag来更新ListScrollingHelper
@State var activeFlag: Bool = false
var body: some View {
VStack {
HStack {
// 通过按钮滚动到顶部
Button(action: { self.scrollingProxy.scrollTo(.top) }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
// 通过按钮滚动到底部
Button(action: { self.scrollingProxy.scrollTo(.end) }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
ScrollView {
ForEach(0 ..< 200) { i in
Text("Item \(i)")
.background(
ListScrollingHelper(activeFlag: self.activeFlag, proxy: self.scrollingProxy))
)
}
}
}
}.onAppear() {
// 重要,延迟0.5秒是为了等待view渲染完以后,再调用ListScrollingHelper
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// 通过改变这个flag,激活ListScrollingHelper的 updateUIView 方法。
self.activeFlag.toggle()
}
// view初始化时 自己滚动到底部。
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
self.scrollingProxy.scrollTo(.end)
}
}
}
struct ListScrollingHelper: UIViewRepresentable {
let activeFlag: Bool
let proxy: ListScrollingProxy // reference type
func makeUIView(context: Context) -> UIView {
return UIView() // managed by SwiftUI, no overloads
}
func updateUIView(_ uiView: UIView, context: Context) {
proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
}
}
import Foundation
import SwiftUI
// 可以实现UIScrollViewDelegate协议
class ListScrollingProxy: NSObject, UIScrollViewDelegate {
enum Action {
case end
case top
case point(point: Double) // << bonus !!
}
// weak重要,要不会会和scrollView发生强引用 造成内存泄露
weak private var scrollView: UIScrollView?
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
// 把捕获到的scrollView的代理设置成该类对象
scrollView?.delegate = self
}
}
// 当scrollView滚动时, 即可获得该scrollView的Offset
func scrollViewDidScroll(_ scrollView: UIScrollView) {
NotificationCenter.default.post(name: .sendScrollViewOffSet, object: (scrollView.contentOffset.x), userInfo: nil)
}
func scrollTo(_ action: Action) {
if let scroller = scrollView {
var rect = CGRect(x: 0, y: 0, width: 1, height: 1)
switch action {
case .end:
rect.origin.x = scroller.contentSize.width +
scroller.contentInset.right + scroller.contentInset.left - 1
case .point(let point):
rect.origin.x = CGFloat(point) >= scroller.contentSize.width ? scroller.contentSize.width - 1: CGFloat(point)
default: {
// default goes to top
}()
}
scroller.scrollRectToVisible(rect, animated: true)
}
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
iOS14:
iOS14多了ScrollViewReader, 实现起来就容易的多了。
class ScrollToModel: ObservableObject {
enum Action {
case end
case top
}
@Published var direction: Action? = nil
}
struct ContentView: View {
@StateObject var vm = ScrollToModel()
let items = (0..<200).map { $0 }
var body: some View {
VStack {
HStack {
Button(action: { vm.direction = .top }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { vm.direction = .end }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
ScrollView {
ScrollViewReader { sp in
LazyVStack {
ForEach(items, id: \.self) { item in
VStack(alignment: .leading) {
Text("Item \(item)").id(item)
Divider()
}.frame(maxWidth: .infinity).padding(.horizontal)
}
}.onReceive(vm.$direction) { action in
guard !items.isEmpty else { return }
withAnimation {
switch action {
case .top:
sp.scrollTo(items.first!, anchor: .top)
case .end:
sp.scrollTo(items.last!, anchor: .bottom)
default:
return
}
}
}
}
}
}
}
}