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

时间:2018-12-19 05:12:59

标签: ios swift uiscrollview

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

- UIViewController
      - UIScrollView (ScrollViewA)
           - UIViewController (ProfileOverviewViewController)
           - UIViewController (ProfileDetailViewController)
               - UICollectionView (ScrollViewB)

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

ScrollViewA上启用了分页功能,因此可以捕捉到ProfileOverviewViewController的整个屏幕视图或ProfileDetailViewController的整个屏幕视图。

希望此图可使布局更加清晰: Layout

我的问题是:

  • 如果用户滚动到ScrollViewA的底部以使ProfileDetailViewControllerScrollViewB可见。
  • 用户在ScrollViewB上向下滚动然后释放。
  • 然后用户在ScrollViewB上滚动。
  • 在用户按住ScrollViewB的内容顶部时,ScrollViewB应该停止滚动,而ScrollViewA应该开始向{{1}滚动},所有这些操作都与用户的手指手势相同。

由于ProfileOverviewViewController属性为true,因此现在ScrollViewB不会简单地扩展到y的负偏移量。

当到达顶部时,如何将ScrollViewB的滚动转移到ScrollViewA?

预先感谢

2 个答案:

答案 0 :(得分:1)

这是一个很好的问题,我不得不花点时间找到合适的解决方案。这是注释的代码。这个想法是向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
    }
}

GlobalViewController:

//
//  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)")
        }
    }
}

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

答案 1 :(得分:1)

下面是一个示例,其中对OP列出的结构进行了稍微修改。我在这里更新了结构:

- UIPageViewController
      - UIViewController (ProfileOverviewViewController)
      - UIViewController (ProfileDetailViewController)
            - UICollectionView (ScrollViewB)

我们用UIPageViewController的实例替换了包含滚动视图的父视图控制器。进行此更改的目的是获得分页功能,以及UIPageViewControllerDataSourceUIPageViewControllerDelegate功能。


//: A UIKit based Playground for presenting user interface

import UIKit
import PlaygroundSupport

// MARK: - ScrollViewAController

final class ScrollViewAController : UIPageViewController {

    private var _viewControllers: [UIViewController] = []

    convenience init(viewControllers: [UIViewController]) {
        self.init(transitionStyle: .scroll, navigationOrientation: .vertical, options: nil)
        self._viewControllers = viewControllers
        dataSource = self
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setViewControllers([_viewControllers.first!], direction: .forward, animated: true, completion: nil)
    }
}

// MARK: UIPageViewControllerDataSource

extension ScrollViewAController: UIPageViewControllerDataSource {

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {

        let viewControllers = _viewControllers
        guard let index = _viewControllers.index(of: viewController) else {
            return nil // view controller not found
        }

        let previousIndex = index - 1
        guard previousIndex >= 0 else {
            return nil // index is invalid
        }

        guard viewControllers.count > previousIndex else {
            return nil // previous index is invalid
        }

        return viewControllers[previousIndex]
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {

        let viewControllers = _viewControllers
        guard let index = viewControllers.index(of: viewController) else {
            return nil // view controller not found
        }

        let nextIndex = index + 1
        let viewControllersCount = viewControllers.count
        guard viewControllersCount != nextIndex else {
            return nil // next index is out-of-bounds (we're at the last page)
        }

        guard viewControllersCount > nextIndex else {
            return nil // next index is invalid
        }

        return viewControllers[nextIndex]
    }
}

// MARK: - ProfileOverviewViewController

final class ProfileOverviewViewController : UIViewController {

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .green

        let label = UILabel()
        label.text = "ProfileOverviewViewController"
        label.textAlignment = .center
        label.textColor = .white
        view.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

        self.view = view
    }
}

// MARK: - ProfileDetailViewController

final class ProfileDetailViewController : UIViewController {

    var scrollViewB: UIScrollView! // should be a collection view, but simplified for this sample.

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .orange

        let label = UILabel()
        label.text = "ProfileDetailViewController"
        label.textAlignment = .center
        label.textColor = .white
        view.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        label.heightAnchor.constraint(equalToConstant: 100).isActive = true

        scrollViewB = UIScrollView()
        scrollViewB.backgroundColor = .blue
        view.addSubview(scrollViewB)

        scrollViewB.translatesAutoresizingMaskIntoConstraints = false
        scrollViewB.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
        scrollViewB.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50).isActive = true
        scrollViewB.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50).isActive = true
        scrollViewB.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

        let scrollViewBLabel = UILabel()
        scrollViewBLabel.numberOfLines = 0
        scrollViewBLabel.text = "ScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\nScrollViewB\n"
        scrollViewBLabel.textAlignment = .center
        scrollViewBLabel.textColor = .white
        scrollViewB.addSubview(scrollViewBLabel)

        scrollViewBLabel.translatesAutoresizingMaskIntoConstraints = false
        scrollViewBLabel.topAnchor.constraint(equalTo: scrollViewB.topAnchor).isActive = true
        scrollViewBLabel.leadingAnchor.constraint(equalTo: scrollViewB.leadingAnchor).isActive = true
        scrollViewBLabel.trailingAnchor.constraint(equalTo: scrollViewB.trailingAnchor).isActive = true
        scrollViewBLabel.heightAnchor.constraint(equalToConstant: 1500).isActive = true
        scrollViewBLabel.bottomAnchor.constraint(equalTo: scrollViewB.bottomAnchor).isActive = true

        self.view = view
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollViewB.contentSize = CGSize(width: view.frame.width, height: 2000)
    }
}

// Present the view controller in the Live View window
let viewControllers: [UIViewController] = [
    ProfileOverviewViewController(),
    ProfileDetailViewController(),
]

PlaygroundPage.current.liveView = ScrollViewAController(viewControllers: viewControllers)