我有一个具有以下结构的接口,括号中的重要元素名称:
- UIViewController
- UIScrollView (ScrollViewA)
- UIViewController (ProfileOverviewViewController)
- UIViewController (ProfileDetailViewController)
- UICollectionView (ScrollViewB)
因此,基本上在另一个垂直滚动视图(ScrollViewB
)内有一个垂直滚动视图(ScrollViewA
)。 ProfileOverviewViewController
和ProfileDetailViewController
都是设备屏幕的大小,因此ScrollViewB
仅在ScrollViewA
滚动到底部时可见。
在ScrollViewA
上启用了分页功能,因此可以捕捉到ProfileOverviewViewController
的整个屏幕视图或ProfileDetailViewController
的整个屏幕视图。
我的问题是:
ScrollViewA
的底部以使ProfileDetailViewController
和ScrollViewB
可见。ScrollViewB
上向下滚动然后释放。ScrollViewB
上滚动。ScrollViewB
的内容顶部时,ScrollViewB
应该停止滚动,而ScrollViewA
应该开始向{{1}滚动},所有这些操作都与用户的手指手势相同。由于ProfileOverviewViewController
属性为true,因此现在ScrollViewB
不会简单地扩展到y的负偏移量。
当到达顶部时,如何将ScrollViewB的滚动转移到ScrollViewA?
预先感谢
答案 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的实例替换了包含滚动视图的父视图控制器。进行此更改的目的是获得分页功能,以及UIPageViewControllerDataSource和UIPageViewControllerDelegate功能。
//: 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)