如何滚动到 SwiftUI 包装器中的 UIScrollView 位置?

How to scroll to position UIScrollView in Wrapper for SwiftUI?

我有一个来自 UIKit 的 ScrollView 并将其用于 SwiftUI:Is there any way to make a paged ScrollView in SwiftUI?

问题:我如何在 UIScrollView 中滚动到一个按钮单击 SwiftUI 视图中的按钮的位置或者什么也适合我需要滚动到首次显示 ScrollView 时的位置

我尝试了 contentOffset 但这没有用。也许我做错了什么。

ScrollViewWrapper:

class UIScrollViewViewController: UIViewController {
    lazy var scrollView: UIScrollView = {
        let v = UIScrollView()
        v.isPagingEnabled = false
        v.alwaysBounceVertical = true
        return v
    }()

    var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.scrollView)
        self.pinEdges(of: self.scrollView, to: self.view)

        self.hostingController.willMove(toParent: self)
        self.scrollView.addSubview(self.hostingController.view)
        self.pinEdges(of: self.hostingController.view, to: self.scrollView)
        self.hostingController.didMove(toParent: self)

    }

    func pinEdges(of viewA: UIView, to viewB: UIView) {
        viewA.translatesAutoresizingMaskIntoConstraints = false
        viewB.addConstraints([
        viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
        viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
        viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
        viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
        ])
    }

}

struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {

    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> UIScrollViewViewController {
        let vc = UIScrollViewViewController()
        vc.hostingController.rootView = AnyView(self.content())
        return vc
    }

    func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
        viewController.hostingController.rootView = AnyView(self.content())
    }
}

SwiftUI 用法:

struct ContentView: View{
    @ObservedObject var search = SearchBar()
    var body: some View{
       NavigationView{
        GeometryReader{geo in
            UIScrollViewWrapper{      //<-----------------
                VStack{
                    ForEach(0..<10){i in
                        Text("lskdfj")
                    }
                }
                .frame(width: geo.size.width)
            }
            .navigationBarTitle("Test")
        }
       }
    }
}

您需要将 @Binding var offset: CGPoint 传递到 UIScrollViewWrapper 然后当在 SwiftUI 视图中单击按钮时,您可以更新绑定值,然后可以在更新方法中使用UIViewControllerRepresentable。另一个想法是改用 UIViewRepresentable 并将其与 UIScrollView 一起使用。这是一篇有用的文章,用于执行此操作并设置其偏移量:https://www.fivestars.blog/articles/scrollview-offset/.

我们将首先在 UIViewControllerRepresentable 中声明 offset 属性,使用 属性Wrapper @Binding,因为它的值可以通过scrollview 或通过父视图(ContentView)。

struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
    var content: () -> Content
    @Binding var offset: CGPoint
    init(offset: Binding<CGPoint>, @ViewBuilder content: @escaping () -> Content) {
        self.content = content
        _offset = offset
    }
// ....//
}

如果父视图的偏移量发生变化,我们必须将这些更改应用于updateUIViewController函数中的scrollView(当视图状态发生变化时调用):

func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
    viewController.hostingController.rootView = AnyView(content())
    viewController.scrollView.contentOffset = offset
}

当偏移量因为用户滚动而改变时,我们必须在我们的 Binding 上反映这个改变。为此,我们必须声明一个 Coordinator,这将是一个 UIScrollViewDelegate,并在其 scrollViewDidScroll 函数中修改偏移量:

class Controller: NSObject, UIScrollViewDelegate {
    var parent: UIScrollViewWrapper<Content>
    init(parent: UIScrollViewWrapper<Content>) {
        self.parent = parent
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        parent.offset = scrollView.contentOffset
    }
}

并且,在 struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable

func makeCoordinator() -> Controller {
    return Controller(parent: self)
}

最后,对于初始偏移量(这很重要,否则您的起始偏移量将始终为 0),这发生在 makeUIViewController 中: 你必须添加这些行:

vc.view.layoutIfNeeded ()
vc.scrollView.contentOffset = offset

最终项目:

import SwiftUI

struct ContentView: View {
    @State private var offset: CGPoint = CGPoint(x: 0, y: 200)
    let texts: [String] = (1...100).map {_ in String.random(length: Int.random(in: 6...20))}
    var body: some View {
        ZStack(alignment: .top) {
            GeometryReader { geo in
                UIScrollViewWrapper(offset: $offset) { //
                    VStack {
                        Text("Start")
                            .foregroundColor(.red)
                        ForEach(texts, id: \.self) { text in
                            Text(text)
                        }
                    }
                        .padding(.top, 40)
                    
                    .frame(width: geo.size.width)
                }
                .navigationBarTitle("Test")
                
            }
            HStack {
                Text(offset.debugDescription)
                Button("add") {
                    offset.y += 100
                }
            }
            .padding(.bottom, 10)
            .frame(maxWidth: .infinity)
            .background(Color.white)
        }
    }
}

class UIScrollViewViewController: UIViewController {
    lazy var scrollView: UIScrollView = {
        let v = UIScrollView()
        v.isPagingEnabled = false
        v.alwaysBounceVertical = true
        return v
    }()

    var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(scrollView)
        pinEdges(of: scrollView, to: view)

        hostingController.willMove(toParent: self)
        scrollView.addSubview(hostingController.view)
        pinEdges(of: hostingController.view, to: scrollView)
        hostingController.didMove(toParent: self)
    }

    func pinEdges(of viewA: UIView, to viewB: UIView) {
        viewA.translatesAutoresizingMaskIntoConstraints = false
        viewB.addConstraints([
            viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
            viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
            viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
            viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
        ])
    }
}

struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
    var content: () -> Content
    @Binding var offset: CGPoint
    init(offset: Binding<CGPoint>, @ViewBuilder content: @escaping () -> Content) {
        self.content = content
        _offset = offset
    }

    func makeCoordinator() -> Controller {
        return Controller(parent: self)
    }

    func makeUIViewController(context: Context) -> UIScrollViewViewController {
        let vc = UIScrollViewViewController()
        vc.scrollView.contentInsetAdjustmentBehavior = .never
        vc.hostingController.rootView = AnyView(content())
        vc.view.layoutIfNeeded()
        vc.scrollView.contentOffset = offset
        vc.scrollView.delegate = context.coordinator
        return vc
    }

    func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
        viewController.hostingController.rootView = AnyView(content())
        viewController.scrollView.contentOffset = offset
    }

    class Controller: NSObject, UIScrollViewDelegate {
        var parent: UIScrollViewWrapper<Content>
        init(parent: UIScrollViewWrapper<Content>) {
            self.parent = parent
        }

        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            parent.offset = scrollView.contentOffset
        }
    }
}