将 UIScrollView 的交互转移到另一个 UIScrollView 中滚动

jac*_*zki 6 uiscrollview ios swift

我有一个具有以下结构的接口,括号中是重要元素的名称:

- UIViewController
      - UIScrollView (ScrollViewA)
           - UIViewController (ProfileOverviewViewController)
           - UIViewController (ProfileDetailViewController)
               - UICollectionView (ScrollViewB)
Run Code Online (Sandbox Code Playgroud)

所以本质上ScrollViewB在另一个垂直滚动视图 ( ScrollViewA) 中有一个垂直滚动视图 ( )。ProfileOverviewViewControllerProfileDetailViewController都是设备屏幕的大小,因此ScrollViewB只有ScrollViewA滚动到底部时才可见。

分页在 上启用ScrollViewA,因此会捕捉到ProfileOverviewViewController整个屏幕视图或ProfileDetailViewController整个屏幕视图。

希望这张图能让布局更清晰一点: 布局

我的问题是:

  • 如果用户滚动至底部ScrollViewA,这样ProfileDetailViewControllerScrollViewB可见。
  • 用户向下滚动一点ScrollViewB然后释放。
  • 然后用户向上滚动ScrollViewB
  • 在仍然按住手指的同时,当用户到达 中内容的顶部时ScrollViewBScrollViewB应该停止滚动并ScrollViewA开始向上滚动到ProfileOverviewViewController,所有这些都在用户的同一手指手势内。

ScrollViewB由于bounces属性为真,此时 where将简单地扩展到负的 y 内容偏移量。

当 ScrollViewB 到达顶部时,如何将其滚动到 ScrollViewA?

提前致谢

Bio*_*che 5

这是一个很好的问题,我不得不深入挖掘才能找到合适的解决方案。这是注释的代码。这个想法是向 scrollViewB 添加自定义平移手势并将 ProfileDetailViewController 设置为其手势委托。当平移将 scrollViewB 带到其顶部时,ProfileOverviewViewController 会收到警告并开始滚动 scrollViewA。当用户松开手指 ProfileOverviewViewController 决定是滚动到内容的底部还是顶部。

希望能帮助到你 :)

ProfileDetailViewController :

//
//  ProfileDetailViewController.swift
//  Sandbox
//
//  Created by Eric Blachère on 23/12/2018.
//  Copyright © 2018 Eric Blachère. All rights reserved.
//

import UIKit

protocol OverflowDelegate: class {
    func onOverflowEnded()
    func onOverflow(delta: CGFloat)
}

/// State of overflow of scrollView
///
/// - on: The scrollview is overflowing : ScrollViewA should take the lead. We store the last trnaslation of the gesture
/// - off: No overflow detected
enum OverflowState {
    case on(lastRecordedGestureTranslation: CGFloat)
    case off

    var isOn: Bool {
        switch self {
        case .on:
            return true
        case .off:
            return false
        }
    }
}

class ProfileDetailViewController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {

    @IBOutlet weak var scrollviewB: UIScrollView!

    weak var delegate: OverflowDelegate?

    /// a pan gesture added on scrollView B
    var customPanGesture: UIPanGestureRecognizer!
    /// The state of the overflow
    var overflowState = OverflowState.off

    override func viewDidLoad() {
        super.viewDidLoad()

        // create a custom pan gesture recognizer added on scrollview B. This way we can be delegate of this gesture & follow the finger
        customPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panRecognized(gesture:)))
        scrollviewB.addGestureRecognizer(customPanGesture)
        customPanGesture.delegate = self

        scrollviewB.delegate = self
    }


    @objc func panRecognized(gesture: UIPanGestureRecognizer) {
        switch overflowState {
        case .on(let lastRecordedGestureTranslation):
            // the user just released his finger
            if gesture.state == .ended {
                print("didEnd !!")
                delegate?.onOverflowEnded() // warn delegate
                overflowState = .off // end of overflow
                scrollviewB.panGestureRecognizer.isEnabled = true // enable scroll again
                return
            }

            // compute the translation delta & send it to delegate
            let fullTranslationY = gesture.translation(in: view).y
            let delta = fullTranslationY - lastRecordedGestureTranslation
            overflowState = .on(lastRecordedGestureTranslation: fullTranslationY)
            delegate?.onOverflow(delta: delta)
        case .off:
            return
        }
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        if scrollView.contentOffset.y <= 0 { // scrollview B is at the top
            // if the overflow is starting : initilize
            if !overflowState.isOn {
                let translation = self.customPanGesture.translation(in: self.view)
                self.overflowState = .on(lastRecordedGestureTranslation: translation.y)

                // disable scroll as we don't scroll in this scrollView from now on
                scrollView.panGestureRecognizer.isEnabled = false
            }
        }
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true // so that both the pan gestures on scrollview will be triggered
    }
}
Run Code Online (Sandbox Code Playgroud)

全局视图控制器:

//
//  GlobalViewController.swift
//  Sandbox
//
//  Created by Eric Blachère on 23/12/2018.
//  Copyright © 2018 Eric Blachère. All rights reserved.
//

import UIKit

class GlobalViewController: UIViewController, OverflowDelegate {

    @IBOutlet weak var scrollViewA: UIScrollView!

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard segue.identifier == "secondSegue", let ctrl = segue.destination as? ProfileDetailViewController else {
            return
        }
        ctrl.delegate = self
    }

    func onOverflowEnded() {
        // scroll to top if at least one third of the overview is showed (you can change this fraction as you please ^^)
        let shouldScrollToTop = (scrollViewA.contentOffset.y <= 2 * scrollViewA.frame.height / 3)
        if shouldScrollToTop {
            scrollViewA.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: true)
        } else {
            scrollViewA.scrollRectToVisible(CGRect(x: 0, y: scrollViewA.contentSize.height - 1, width: 1, height: 1), animated: true)
        }
    }

    func onOverflow(delta: CGFloat) {
        // move the scrollview content
        if scrollViewA.contentOffset.y - delta <= scrollViewA.contentSize.height - scrollViewA.frame.height {
            scrollViewA.contentOffset.y -= delta
            print("difference : \(delta)")
            print("contentOffset : \(scrollViewA.contentOffset.y)")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑: ProfileOverviewViewController 和 ProfileDetailViewController 是通过容器视图在故事板中的 GlobalViewController 中设置的,但如果在代码中设置,它应该也能正常工作;)