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