SwiftUI

SwiftUI ScrollView通过代码滚动

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