如何在SwiftUI中创建同步的ScrollView

5

我正在尝试在SwiftUI中创建两个同步的ScrollViews,以便在一个中滚动会导致另一个也滚动。

我在使用底部显示的ScrollViewOffset类获取scrollView偏移值时遇到了问题,但不知道如何滚动另一个视图。

我似乎可以通过阻止一个视图的滚动并在另一个视图上设置内容位置(position())来“hack”它 - 是否有任何方法实际滚动scrollView内容到位置? 我知道ScrollViewReader似乎允许滚动以显示内容项,但我似乎找不到任何将内容滚动到偏移位置的东西。

使用position()的问题是它实际上不会更改ScrollViews滚动条位置 - 似乎没有ScrollView.scrollContentsTo(point: CGPoint)。

 @State private var scrollOffset1: CGPoint = .zero 

 HStack {
   ScrollViewOffset(onOffsetChange: { offset in
                    scrollOffset1 = offset
                    print("New ScrollView1 offset: \(offset)")
                }, content: {
                    
                    VStack {
                        
                        ImageView(filteredImageProvider: self.provider)
                            .frame(width: imageWidth, height: imageHeight)
                    }
                    .frame(width: imageWidth + (geometry.size.width - 20) * 2, height: imageHeight + (geometry.size.height - 20) * 2)
                    .border(Color.white)
                    .id(0)
   })

   ScrollView([]) {
                
                VStack {
                    
                    ImageView(filteredImageProvider: self.provider, showEdits: false)
                        .frame(width: imageWidth, height: imageHeight)
                }
                .frame(width: imageWidth + (geometry.size.width - 20) * 2, height: imageHeight + (geometry.size.height - 20) * 2)
                .border(Color.white)
                .id(0)
                .position(x: scrollOffset1.x, y: scrollOffset1.y + (imageHeight + (geometry.size.height - 20) * 2)/2)
                
     }

}


//
//  ScrollViewOffset.swift
//  ZoomView
//
//

import Foundation
import SwiftUI

struct ScrollViewOffset<Content: View>: View {
    let onOffsetChange: (CGPoint) -> Void
    let content: () -> Content
    
    init(
        onOffsetChange: @escaping (CGPoint) -> Void,
        @ViewBuilder content: @escaping () -> Content
    ) {
        self.onOffsetChange = onOffsetChange
        self.content = content
    }
    
    var body: some View {
        ScrollView([.horizontal, .vertical]) {
            offsetReader
            content()
                .padding(.top, -8)
        }
        .coordinateSpace(name: "frameLayer")
        .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: onOffsetChange)
    }
    
    var offsetReader: some View {
        GeometryReader { proxy in
            Color.clear
                .preference(
                    key: ScrollOffsetPreferenceKey.self,
                    value: proxy.frame(in: .named("frameLayer")).origin
                )
        }
        .frame(width: 0, height: 0)
    }
}

private struct ScrollOffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGPoint = .zero
    static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
}
2个回答

12

同步滚动视图。

在此示例中,您可以滚动左侧滚动视图,右侧滚动视图将与相同位置同步。在此示例中,右侧的滚动视图已禁用,并且通过使用偏移量简单地同步了位置。

但是,使用相同的逻辑和代码,当其中一个滚动视图被滚动时,您可以使左侧和右侧的滚动视图都同步。

import SwiftUI

struct ContentView: View {
    
    @State private var offset = CGFloat.zero
    
    var body: some View {
        
        HStack(alignment: .top) {
            
            // MainScrollView
            ScrollView {
                VStack {
                    ForEach(0..<100) { i in
                        Text("Item \(i)").padding()
                    }
                }
                .background( GeometryReader {
                    Color.clear.preference(key: ViewOffsetKey.self,
                                           value: -$0.frame(in: .named("scroll")).origin.y)
                })
                .onPreferenceChange(ViewOffsetKey.self) { value in
                    print("offset >> \(value)")
                    offset = value
                }
            }
            .coordinateSpace(name: "scroll")
            
            
            // Synchronised with ScrollView above
            ScrollView {
                VStack {
                    ForEach(0..<100) { i in
                        Text("Item \(i)").padding()
                    }
                }
                .offset(y: -offset)
            }
            .disabled(true)
        }
    }
}

struct ViewOffsetKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

0

根据当前ScrollView的API,这是不可能实现的。虽然你可以使用互联网上广泛可用的方法获取scrollView的contentOffset,但用于以编程方式滚动ScrollView的ScrollViewReader仅允许你滚动到特定视图,而不是到contentOffset。 要实现此功能,您需要包装UIScrollView。以下是一种实现方式,尽管它不是100%稳定,并且缺少大量的scrollView功能:

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

            }

        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接