使用ScrollViewReader自定义scrollTo

使用ScrollViewReader自定义scrollTo

本文介绍了SwiftUI |使用ScrollViewReader自定义scrollTo()的动画?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

是否可以通过使用 ScrollViewReader 来更改 ScrollView 的默认滚动动画?

Is there a way to change the default scrolling animation of the ScrollView by using ScrollViewReader?

我尝试了不同的方法,但是动画仍然是默认设置.

I have tried different things but the animation remained as the default one.

withAnimation(.easeInOut(duration: 60)) { // <-- Not working (changes nothing)
    proxy.scrollTo(50, anchor: .center)
}

正如您在此处看到的:(显然,这比1分钟的动画要快)

As you can see here: (Obviously this is faster than a 1 minute animation)

struct ContentView: View {

    var body: some View {
        ScrollView {
            ScrollViewReader { proxy in
                Button("Scroll to") {
                    withAnimation(.easeInOut(duration: 60)) {
                        proxy.scrollTo(50, anchor: .center)
                    }
                }

                ForEach(0..<100) { i in
                    Rectangle()
                        .frame(width: 200, height: 100)
                        .foregroundColor(.green)
                        .overlay(Text("\(i)").foregroundColor(.white).id(i))
                }
                .frame(maxWidth: .infinity)
            }
        }
    }
}

也许这还不可能吗?

谢谢!

推荐答案

前段时间我遇到了同样的问题,并且我在GitHub上找到了以下代码:

I had the same problem sometime ago, and I found this code in GitHub:

可滚动的SwuifUI包装器

使用以下代码在Swift文件中创建自定义UIScrollView:

Create a custom UIScrollView in a Swift file with this code:

import SwiftUI

struct ScrollableView<Content: View>: UIViewControllerRepresentable, Equatable {

    // MARK: - Coordinator
    final class Coordinator: NSObject, UIScrollViewDelegate {

        // MARK: - Properties
        private let scrollView: UIScrollView
        var offset: Binding<CGPoint>

        // MARK: - Init
        init(_ scrollView: UIScrollView, offset: Binding<CGPoint>) {
            self.scrollView          = scrollView
            self.offset              = offset
            super.init()
            self.scrollView.delegate = self
        }

        // MARK: - UIScrollViewDelegate
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            DispatchQueue.main.async {
                self.offset.wrappedValue = scrollView.contentOffset
            }
        }
    }

    // MARK: - Type
    typealias UIViewControllerType = UIScrollViewController<Content>

    // MARK: - Properties
    var offset: Binding<CGPoint>
    var animationDuration: TimeInterval
    var showsScrollIndicator: Bool
    var axis: Axis
    var content: () -> Content
    var onScale: ((CGFloat)->Void)?
    var disableScroll: Bool
    var forceRefresh: Bool
    var stopScrolling: Binding<Bool>
    private let scrollViewController: UIViewControllerType

    // MARK: - Init
    init(_ offset: Binding<CGPoint>, animationDuration: TimeInterval, showsScrollIndicator: Bool = true, axis: Axis = .vertical, onScale: ((CGFloat)->Void)? = nil, disableScroll: Bool = false, forceRefresh: Bool = false, stopScrolling: Binding<Bool> = .constant(false),  @ViewBuilder content: @escaping () -> Content) {
        self.offset               = offset
        self.onScale              = onScale
        self.animationDuration    = animationDuration
        self.content              = content
        self.showsScrollIndicator = showsScrollIndicator
        self.axis                 = axis
        self.disableScroll        = disableScroll
        self.forceRefresh         = forceRefresh
        self.stopScrolling        = stopScrolling
        self.scrollViewController = UIScrollViewController(rootView: self.content(), offset: self.offset, axis: self.axis, onScale: self.onScale)
    }

    // MARK: - Updates
    func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> UIViewControllerType {
        self.scrollViewController
    }

    func updateUIViewController(_ viewController: UIViewControllerType, context: UIViewControllerRepresentableContext<Self>) {

        viewController.scrollView.showsVerticalScrollIndicator   = self.showsScrollIndicator
        viewController.scrollView.showsHorizontalScrollIndicator = self.showsScrollIndicator
        viewController.updateContent(self.content)

        let duration: TimeInterval                = self.duration(viewController)
        let newValue: CGPoint                     = self.offset.wrappedValue
        viewController.scrollView.isScrollEnabled = !self.disableScroll

        if self.stopScrolling.wrappedValue {
            viewController.scrollView.setContentOffset(viewController.scrollView.contentOffset, animated:false)
            return
        }

        guard duration != .zero else {
            viewController.scrollView.contentOffset = newValue
            return
        }

        UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .curveEaseInOut, .beginFromCurrentState], animations: {
            viewController.scrollView.contentOffset = newValue
        }, completion: nil)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self.scrollViewController.scrollView, offset: self.offset)
    }

    //Calcaulte max offset
    private func newContentOffset(_ viewController: UIViewControllerType, newValue: CGPoint) -> CGPoint {

        let maxOffsetViewFrame: CGRect = viewController.view.frame
        let maxOffsetFrame: CGRect     = viewController.hostingController.view.frame
        let maxOffsetX: CGFloat        = maxOffsetFrame.maxX - maxOffsetViewFrame.maxX
        let maxOffsetY: CGFloat        = maxOffsetFrame.maxY - maxOffsetViewFrame.maxY

        return CGPoint(x: min(newValue.x, maxOffsetX), y: min(newValue.y, maxOffsetY))
    }

    //Calculate animation speed
    private func duration(_ viewController: UIViewControllerType) -> TimeInterval {

        var diff: CGFloat = 0

        switch axis {
            case .horizontal:
                diff = abs(viewController.scrollView.contentOffset.x - self.offset.wrappedValue.x)
            default:
                diff = abs(viewController.scrollView.contentOffset.y - self.offset.wrappedValue.y)
        }

        if diff == 0 {
            return .zero
        }

        let percentageMoved = diff / UIScreen.main.bounds.height

        return self.animationDuration * min(max(TimeInterval(percentageMoved), 0.25), 1)
    }

    // MARK: - Equatable
    static func == (lhs: ScrollableView, rhs: ScrollableView) -> Bool {
        return !lhs.forceRefresh && lhs.forceRefresh == rhs.forceRefresh
    }
}

final class UIScrollViewController<Content: View> : UIViewController, ObservableObject {

    // MARK: - Properties
    var offset: Binding<CGPoint>
    var onScale: ((CGFloat)->Void)?
    let hostingController: UIHostingController<Content>
    private let axis: Axis
    lazy var scrollView: UIScrollView = {

        let scrollView                                       = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.canCancelContentTouches                   = true
        scrollView.delaysContentTouches                      = true
        scrollView.scrollsToTop                              = false
        scrollView.backgroundColor                           = .clear

        if self.onScale != nil {
            scrollView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(self.onGesture)))
        }

        return scrollView
    }()

    @objc func onGesture(gesture: UIPinchGestureRecognizer) {
        self.onScale?(gesture.scale)
    }

    // MARK: - Init
    init(rootView: Content, offset: Binding<CGPoint>, axis: Axis, onScale: ((CGFloat)->Void)?) {
        self.offset                                 = offset
        self.hostingController                      = UIHostingController<Content>(rootView: rootView)
        self.hostingController.view.backgroundColor = .clear
        self.axis                                   = axis
        self.onScale                                = onScale
        super.init(nibName: nil, bundle: nil)
    }

    // MARK: - Update
    func updateContent(_ content: () -> Content) {

        self.hostingController.rootView = content()
        self.scrollView.addSubview(self.hostingController.view)

        var contentSize: CGSize = self.hostingController.view.intrinsicContentSize

        switch axis {
            case .vertical:
                contentSize.width = self.scrollView.frame.width
            case .horizontal:
                contentSize.height = self.scrollView.frame.height
        }

        self.hostingController.view.frame.size = contentSize
        self.scrollView.contentSize            = contentSize
        self.view.updateConstraintsIfNeeded()
        self.view.layoutIfNeeded()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.scrollView)
        self.createConstraints()
        self.view.setNeedsUpdateConstraints()
        self.view.updateConstraintsIfNeeded()
        self.view.layoutIfNeeded()
    }

    // MARK: - Constraints
    fileprivate func createConstraints() {
        NSLayoutConstraint.activate([
            self.scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor),
            self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
        ])
    }
}

然后,您可以在代码段中像这样使用它:

Then in your code snippet you can use it like this:

import SwiftUI

struct ContentView: View {
    @State private var contentOffset: CGPoint = .zero
    var body: some View {
        ScrollableView(self.$contentOffset, animationDuration: 5.0) {
            VStack {
                Button("Scroll to") {
                    self.contentOffset = CGPoint(x: 0, y: (100 * 50))
                }
                ForEach(0..<100) { i in
                    Rectangle()
                        .frame(width: 200, height: 100)
                        .foregroundColor(.green)
                        .overlay(Text("\(i)").foregroundColor(.white).id(i))
                }
                .frame(maxWidth: .infinity)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

您可以设置动画持续时间,将其设置为5秒,然后根据行高计算要滚动的偏移量,将其设置为100 * 50单元格.

You can set the animation duration, I have set it to 5 seconds, then calculate the offset you want to scroll depending in the row height, I set it to 100 * 50 cells.

点击按钮后,视图将在5秒钟内滚动到索引50.

When you tap in the button the view will scroll to index 50 in 5 seconds.

这篇关于SwiftUI |使用ScrollViewReader自定义scrollTo()的动画?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-31 03:36