ScrollViews Sync SwiftUI

Hi, I've got two ScrollViews I need to sync vertical scrolls of these views. When I scroll one the other one should automatically be adjusted to the new scroll position of first one.

Do you have any idea how to implement this behavior ?

I googled I guess the only way to navigate(to scroll to) within scrollView it's through

ScrollViewReader

_proxy.scrollTo(ID_element_in_ScrollView, anchor: .center)




With the current API of ScrollView, this is not possible. While you can get the contentOffset of the scrollView using methods that are widely available on the internet, the ScrollViewReader that is used to programmatically scroll a ScrollView only allows you to scroll to specific views, instead of to a contentOffset.

To achieve this functionality, you are going to have to wrap UIScrollView. Here is an implementation, although it isn't 100% stable, and is missing a good amount of scrollView functionality:

import SwiftUI
import UIKit

public struct ScrollableView<Content: View>: UIViewControllerRepresentable {
    @Binding var offset: CGPoint

    var content: () -> Content

    public init(_ offset: Binding<CGPoint>, @ViewBuilder content: @escaping () -> Content) {
        self._offset = offset
        self.content = content
    }

    public func makeUIViewController(context: Context) -> UIScrollViewViewController {
        let vc = UIScrollViewViewController()
        vc.hostingController.rootView = AnyView(self.content())
        vc.scrollView.setContentOffset(offset, animated: false)
        vc.delegate = context.coordinator
        return vc
    }

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

        // Allow for deaceleration to be done by the scrollView
        if !viewController.scrollView.isDecelerating {
            viewController.scrollView.setContentOffset(offset, animated: false)
        }
    }

    public func makeCoordinator() -> Coordinator {
        Coordinator(contentOffset: _offset)
    }

    public class Coordinator: NSObject, UIScrollViewDelegate {
        let contentOffset: Binding<CGPoint>

        init(contentOffset: Binding<CGPoint>) {
            self.contentOffset = contentOffset
        }

        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
            contentOffset.wrappedValue = scrollView.contentOffset
        }
    }
}

public class UIScrollViewViewController: UIViewController {
    lazy var scrollView: UIScrollView = UIScrollView()

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

    weak var delegate: UIScrollViewDelegate?

    public override func viewDidLoad() {
        super.viewDidLoad()
        self.scrollView.delegate = delegate
        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 ScrollableView_Previews: PreviewProvider {
    static var previews: some View {
        Wrapper()
    }

    struct Wrapper: View {
        @State var offset: CGPoint = .init(x: 0, y: 50)

        var body: some View {
            HStack {
                ScrollableView($offset, content: {
                    ForEach(0...100, id: \.self) { id in
                        Text("\(id)")
                    }
                })

                ScrollableView($offset, content: {
                    ForEach(0...100, id: \.self) { id in
                        Text("\(id)")
                    }
                })

                VStack {
                    Text("x: \(offset.x) y: \(offset.y)")

                    Button("Top", action: {
                        offset = .zero
                    })
                    .buttonStyle(.borderedProminent)
                }
                .frame(width: 200)
                .padding()
            }
        }
    }
}
ScrollViews Sync SwiftUI
 
 
Q