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 } } } } } } } }