Swift - 带有嵌入式 UIStackView 的 UIScrollView 的无限滚动
Swift - Infinite Scrolling for UIScrollView with an embedded UIStackView
有人用下面的代码帮助实现了我在这里需要做的事情:-
在保持现有行为不变的同时,我需要一些帮助,请修改 scrollViewDidScroll 方法以允许无限平滑滚动,因此当您向右滑动到达第四个/最后一个项目时,它将平滑过渡到第一页,同样,如果您在到达第一页时继续向左滑动,然后再次向左滑动,将显示最后一个项目,如果您明白我的意思,我尝试过这样做,但有点过头了。谢谢
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = true
v.bounces = false
return v
}()
let pageControl: UIPageControl = {
let v = UIPageControl()
return v
}()
let stack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
return v
}()
var pages: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
pageControl.translatesAutoresizingMaskIntoConstraints = false
stack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
view.addSubview(scrollView)
view.addSubview(pageControl)
let g = view.safeAreaLayoutGuide
let svCLG = scrollView.contentLayoutGuide
let svFLG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -80.0),
stack.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
stack.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: 0.0),
pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
pageControl.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
pageControl.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
])
// if we're loading "page" view controllers from Storyboard
/*
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFirst") as? PSFirstViewController {
pages.append(vc)
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psSecond") as? PSSecondViewController {
pages.append(vc)
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psThird") as? PSThirdViewController {
pages.append(vc)
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFourth") as? PSFourthViewController {
pages.append(vc)
}
pages.forEach { vc in
self.addChild(vc)
stack.addArrangedSubview(vc.view)
vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
vc.didMove(toParent: self)
}
*/
// for this example,
// create 4 view controllers, with background colors
let colors: [UIColor] = [
.red, .brown, .blue, .magenta
]
colors.forEach { c in
let vc = BasePageController()
vc.view.backgroundColor = c
self.addChild(vc)
stack.addArrangedSubview(vc.view)
vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
vc.didMove(toParent: self)
pages.append(vc)
}
pageControl.numberOfPages = pages.count
scrollView.delegate = self
pageControl.addTarget(self, action: #selector(self.pgControlChange(_:)), for: .valueChanged)
}
var pgControlScroll: Bool = false
@objc func pgControlChange(_ sender: UIPageControl) {
pgControlScroll = true
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
let cp = min(Int(round(x / w)), pages.count - 1)
let np = sender.currentPage
var r = CGRect.zero
if np > cp {
r = CGRect(x: w * CGFloat(np + 1) - 1.0, y: 0, width: 1, height: 1)
} else {
r = CGRect(x: w * CGFloat(np), y: 0, width: 1, height: 1)
}
scrollView.scrollRectToVisible(r, animated: true)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
pgControlScroll = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
let pg = min(Int(round(x / w)), pages.count - 1)
let v = stack.arrangedSubviews[pg]
pageControl.backgroundColor = v.backgroundColor
if pgControlScroll { return }
pageControl.currentPage = pg
}
}
class BasePageController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// add a label at each corner
for (i, s) in ["top-left", "top-right", "bot-left", "bot-right"].enumerated() {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
v.translatesAutoresizingMaskIntoConstraints = false
v.text = s
view.addSubview(v)
let g = view.safeAreaLayoutGuide
switch i {
case 1:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
case 2:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
default:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
}
}
}
}
这是一种方法...
由于滚动视图启用了分页,我们一次只能在滚动视图中显示 3 个视图“页面”。
假设总共有 4 个“页面”...
- 从第 4、1、2 页开始
- 设置滚动内容偏移 x 以便中心页面可见
- 如果我们滚动到下一页,将视图移动到 1、2、3 并再次设置滚动内容偏移 x 以便中心页面可见
- 如果我们滚动到上一页,将视图移动到 3、4、1 并再次设置滚动内容偏移量 x,以便中心页面可见
此方法将所有“页面”视图控制器加载为子视图控制器。如果我们最终得到 20 个“页面”——特别是如果它们“很重”(很多子视图、代码等)——我们希望仅在需要显示它们时加载控制器,并在需要时卸载它们'从 3 个“滚动槽”中移除。
struct MyPage
定义一个页面作为视图控制器和索引页码:
struct MyPage {
var vc: UIViewController!
var pageNumber: Int!
}
PagedScrollViewController class:
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = true
v.showsHorizontalScrollIndicator = false
// set clipsToBounds to false
// if we want to see the way the views are being cycled
v.clipsToBounds = true
return v
}()
let pageControl: UIPageControl = {
let v = UIPageControl()
return v
}()
let stack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
return v
}()
var pages: [MyPage] = []
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
pageControl.translatesAutoresizingMaskIntoConstraints = false
stack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
view.addSubview(scrollView)
view.addSubview(pageControl)
let g = view.safeAreaLayoutGuide
let svCLG = scrollView.contentLayoutGuide
let svFLG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// cover most of screen (with a little padding on each side)
//scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
//scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
//scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
//scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -80.0),
// small scroll view at top of screen
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
scrollView.heightAnchor.constraint(equalToConstant: 200.0),
stack.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
stack.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: 0.0),
pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
pageControl.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
pageControl.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
])
/*
// if we're loading "page" view controllers from Storyboard
var i = 0
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFirst") as? PSFirstViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psSecond") as? PSSecondViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psThird") as? PSThirdViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFourth") as? PSFourthViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
pages.forEach { pg in
self.addChild(pg.vc)
pg.vc.didMove(toParent: self)
}
*/
// for this example, we will
// create 4 "page" view controllers, with background colors
// (dark red, dark green, dark blue, brown)
let colors: [UIColor] = [
UIColor(red: 0.75, green: 0.00, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.00, blue: 0.75, alpha: 1.0),
UIColor(red: 0.75, green: 0.50, blue: 0.00, alpha: 1.0),
]
for (c, i) in zip(colors, Array(0..<colors.count)) {
let vc = BasePageController()
vc.view.backgroundColor = c
vc.centerLabel.text = "\(i + 1)"
self.addChild(vc)
vc.didMove(toParent: self)
pages.append(MyPage(vc: vc, pageNumber: i))
}
// move last page to position Zero
pages.insert(pages.removeLast(), at: 0)
// add 3 pages to stack view in scroll view
pages[0...2].forEach { pg in
stack.addArrangedSubview(pg.vc.view)
pg.vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
}
scrollView.delegate = self
pageControl.numberOfPages = pages.count
pageControl.addTarget(self, action: #selector(self.pgControlChange(_:)), for: .valueChanged)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
scrollView.contentOffset.x = scrollView.frame.size.width
}
// flag so we don't infinite loop on scrolling and setting page control current page
var pgControlScroll: Bool = false
@objc func pgControlChange(_ sender: UIPageControl) {
let w = scrollView.frame.size.width
guard w != 0 else { return }
// get the middle page
let pg = pages[1]
// unwrap current page number in scroll view
guard let cp = pg.pageNumber else { return }
// set the flag
pgControlScroll = true
// next page based on page control page number
let np = sender.currentPage
var r = CGRect.zero
if np > cp {
r = CGRect(x: w * 3.0 - 1.0, y: 0.0, width: 1.0, height: 1.0)
// next page is to the right
} else {
// next page is to the left
r = CGRect(x: 0.0, y: 0, width: 1, height: 1)
}
// need to manually animate the scroll, so we can update our page order when scroll finishes
UIView.animate(withDuration: 0.3, animations: {
self.scrollView.scrollRectToVisible(r, animated: false)
}, completion: { _ in
self.updatePages(self.scrollView)
})
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// turn off the flag
pgControlScroll = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
// get the "page" based on scroll offset x
let pgID = min(Int(round(x / w)), pages.count - 1)
let pg = pages[pgID]
guard let v = pg.vc.view else { return }
pageControl.backgroundColor = v.backgroundColor
// don't set the pageControl's pageNumber if we scrolled as a result of tapping the page control
if pgControlScroll { return }
pageControl.currentPage = pg.pageNumber
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updatePages(scrollView)
}
func updatePages(_ scrollView: UIScrollView) -> Void {
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
if x == 0 {
// we've scrolled to the left
// move last page to position Zero
guard let pg = pages.last,
let v = pg.vc.view else { return }
// remove the last arranged subview
stack.arrangedSubviews.last?.removeFromSuperview()
// insert last "page" view as first arranged subview
stack.insertArrangedSubview(v, at: 0)
// set its width anchor
v.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
// move last page to first position in array
pages.insert(pages.removeLast(), at: 0)
} else if x == scrollView.frame.size.width * 2 {
// we've scrolled right
// move first page to last position in array
pages.append(pages.removeFirst())
// get the next page
let pg = pages[2]
guard let v = pg.vc.view else { return }
// remove first arranged subview
stack.arrangedSubviews.first?.removeFromSuperview()
// add next page view to stack
stack.addArrangedSubview(v)
// set its width anchor
v.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
}
scrollView.contentOffset.x = scrollView.frame.size.width
}
}
BasePageController 示例“页面”class:
class BasePageController: UIViewController {
let centerLabel: UILabel = {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.font = UIFont.systemFont(ofSize: 48.0, weight: .bold)
v.textAlignment = .center
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add a label at each corner
for (i, s) in ["top-left", "top-right", "bot-left", "bot-right"].enumerated() {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
v.translatesAutoresizingMaskIntoConstraints = false
v.text = s
view.addSubview(v)
let g = view.safeAreaLayoutGuide
switch i {
case 1:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
case 2:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
default:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
}
}
centerLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(centerLabel)
NSLayoutConstraint.activate([
centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
备注:
这只是 示例代码,不应被视为“生产就绪”。
如果您只有 1 或 2 个“页面”,这将崩溃。
如果你用几十个“页面”尝试这个,你可能会遇到内存问题。
编辑 根据评论...
查看了您的项目,我发现您使用的是 UICollectionView
。
我认为问题在于您正在混合/匹配 viewModel.pages
- 具有 4
元素,而 itemsWithBoundries
- 具有 6
元素。试图调和那是相当混乱的。
所以...我将建议一种不同的、较旧的方法。
对于集合视图的 numberOfItemsInSection
,我要 return 10,000。
在 cellForItemAt
中,我将使用 indexPath.item % viewModel.pages.count
(余数/取模运算符)来 return viewModel.pages[0...3]
范围内的单元格。
在 scrollViewDidScroll
中有相同的想法 ... 获取实际的单元格项目索引 %
页数,从零到 3。
为了在两个方向上实现“无限滚动”,我将从将集合视图滚动到第 5,000 项开始(代码包括在页数不能等分为 5,000 时进行调整)。用户不太可能在任一方向滚动 5,000 页以到达“结尾”。
我用这种方法编辑了您的测试应用程序并将其发布到 GitHub:https://github.com/DonMag/Test-App 这样您就可以看到我所做的更改。
有人用下面的代码帮助实现了我在这里需要做的事情:-
在保持现有行为不变的同时,我需要一些帮助,请修改 scrollViewDidScroll 方法以允许无限平滑滚动,因此当您向右滑动到达第四个/最后一个项目时,它将平滑过渡到第一页,同样,如果您在到达第一页时继续向左滑动,然后再次向左滑动,将显示最后一个项目,如果您明白我的意思,我尝试过这样做,但有点过头了。谢谢
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = true
v.bounces = false
return v
}()
let pageControl: UIPageControl = {
let v = UIPageControl()
return v
}()
let stack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
return v
}()
var pages: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
pageControl.translatesAutoresizingMaskIntoConstraints = false
stack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
view.addSubview(scrollView)
view.addSubview(pageControl)
let g = view.safeAreaLayoutGuide
let svCLG = scrollView.contentLayoutGuide
let svFLG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -80.0),
stack.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
stack.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: 0.0),
pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
pageControl.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
pageControl.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
])
// if we're loading "page" view controllers from Storyboard
/*
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFirst") as? PSFirstViewController {
pages.append(vc)
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psSecond") as? PSSecondViewController {
pages.append(vc)
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psThird") as? PSThirdViewController {
pages.append(vc)
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFourth") as? PSFourthViewController {
pages.append(vc)
}
pages.forEach { vc in
self.addChild(vc)
stack.addArrangedSubview(vc.view)
vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
vc.didMove(toParent: self)
}
*/
// for this example,
// create 4 view controllers, with background colors
let colors: [UIColor] = [
.red, .brown, .blue, .magenta
]
colors.forEach { c in
let vc = BasePageController()
vc.view.backgroundColor = c
self.addChild(vc)
stack.addArrangedSubview(vc.view)
vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
vc.didMove(toParent: self)
pages.append(vc)
}
pageControl.numberOfPages = pages.count
scrollView.delegate = self
pageControl.addTarget(self, action: #selector(self.pgControlChange(_:)), for: .valueChanged)
}
var pgControlScroll: Bool = false
@objc func pgControlChange(_ sender: UIPageControl) {
pgControlScroll = true
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
let cp = min(Int(round(x / w)), pages.count - 1)
let np = sender.currentPage
var r = CGRect.zero
if np > cp {
r = CGRect(x: w * CGFloat(np + 1) - 1.0, y: 0, width: 1, height: 1)
} else {
r = CGRect(x: w * CGFloat(np), y: 0, width: 1, height: 1)
}
scrollView.scrollRectToVisible(r, animated: true)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
pgControlScroll = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
let pg = min(Int(round(x / w)), pages.count - 1)
let v = stack.arrangedSubviews[pg]
pageControl.backgroundColor = v.backgroundColor
if pgControlScroll { return }
pageControl.currentPage = pg
}
}
class BasePageController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// add a label at each corner
for (i, s) in ["top-left", "top-right", "bot-left", "bot-right"].enumerated() {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
v.translatesAutoresizingMaskIntoConstraints = false
v.text = s
view.addSubview(v)
let g = view.safeAreaLayoutGuide
switch i {
case 1:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
case 2:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
default:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
}
}
}
}
这是一种方法...
由于滚动视图启用了分页,我们一次只能在滚动视图中显示 3 个视图“页面”。
假设总共有 4 个“页面”...
- 从第 4、1、2 页开始
- 设置滚动内容偏移 x 以便中心页面可见
- 如果我们滚动到下一页,将视图移动到 1、2、3 并再次设置滚动内容偏移 x 以便中心页面可见
- 如果我们滚动到上一页,将视图移动到 3、4、1 并再次设置滚动内容偏移量 x,以便中心页面可见
此方法将所有“页面”视图控制器加载为子视图控制器。如果我们最终得到 20 个“页面”——特别是如果它们“很重”(很多子视图、代码等)——我们希望仅在需要显示它们时加载控制器,并在需要时卸载它们'从 3 个“滚动槽”中移除。
struct MyPage
定义一个页面作为视图控制器和索引页码:
struct MyPage {
var vc: UIViewController!
var pageNumber: Int!
}
PagedScrollViewController class:
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = true
v.showsHorizontalScrollIndicator = false
// set clipsToBounds to false
// if we want to see the way the views are being cycled
v.clipsToBounds = true
return v
}()
let pageControl: UIPageControl = {
let v = UIPageControl()
return v
}()
let stack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
return v
}()
var pages: [MyPage] = []
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
pageControl.translatesAutoresizingMaskIntoConstraints = false
stack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
view.addSubview(scrollView)
view.addSubview(pageControl)
let g = view.safeAreaLayoutGuide
let svCLG = scrollView.contentLayoutGuide
let svFLG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// cover most of screen (with a little padding on each side)
//scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
//scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
//scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
//scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -80.0),
// small scroll view at top of screen
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
scrollView.heightAnchor.constraint(equalToConstant: 200.0),
stack.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
stack.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: 0.0),
pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
pageControl.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
pageControl.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
])
/*
// if we're loading "page" view controllers from Storyboard
var i = 0
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFirst") as? PSFirstViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psSecond") as? PSSecondViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psThird") as? PSThirdViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
if let vc = storyboard?.instantiateViewController(withIdentifier: "psFourth") as? PSFourthViewController {
pages.append(MyPage(vc: vc, pageNumber: i))
i += 1
}
pages.forEach { pg in
self.addChild(pg.vc)
pg.vc.didMove(toParent: self)
}
*/
// for this example, we will
// create 4 "page" view controllers, with background colors
// (dark red, dark green, dark blue, brown)
let colors: [UIColor] = [
UIColor(red: 0.75, green: 0.00, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.00, blue: 0.75, alpha: 1.0),
UIColor(red: 0.75, green: 0.50, blue: 0.00, alpha: 1.0),
]
for (c, i) in zip(colors, Array(0..<colors.count)) {
let vc = BasePageController()
vc.view.backgroundColor = c
vc.centerLabel.text = "\(i + 1)"
self.addChild(vc)
vc.didMove(toParent: self)
pages.append(MyPage(vc: vc, pageNumber: i))
}
// move last page to position Zero
pages.insert(pages.removeLast(), at: 0)
// add 3 pages to stack view in scroll view
pages[0...2].forEach { pg in
stack.addArrangedSubview(pg.vc.view)
pg.vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
}
scrollView.delegate = self
pageControl.numberOfPages = pages.count
pageControl.addTarget(self, action: #selector(self.pgControlChange(_:)), for: .valueChanged)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
scrollView.contentOffset.x = scrollView.frame.size.width
}
// flag so we don't infinite loop on scrolling and setting page control current page
var pgControlScroll: Bool = false
@objc func pgControlChange(_ sender: UIPageControl) {
let w = scrollView.frame.size.width
guard w != 0 else { return }
// get the middle page
let pg = pages[1]
// unwrap current page number in scroll view
guard let cp = pg.pageNumber else { return }
// set the flag
pgControlScroll = true
// next page based on page control page number
let np = sender.currentPage
var r = CGRect.zero
if np > cp {
r = CGRect(x: w * 3.0 - 1.0, y: 0.0, width: 1.0, height: 1.0)
// next page is to the right
} else {
// next page is to the left
r = CGRect(x: 0.0, y: 0, width: 1, height: 1)
}
// need to manually animate the scroll, so we can update our page order when scroll finishes
UIView.animate(withDuration: 0.3, animations: {
self.scrollView.scrollRectToVisible(r, animated: false)
}, completion: { _ in
self.updatePages(self.scrollView)
})
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// turn off the flag
pgControlScroll = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
// get the "page" based on scroll offset x
let pgID = min(Int(round(x / w)), pages.count - 1)
let pg = pages[pgID]
guard let v = pg.vc.view else { return }
pageControl.backgroundColor = v.backgroundColor
// don't set the pageControl's pageNumber if we scrolled as a result of tapping the page control
if pgControlScroll { return }
pageControl.currentPage = pg.pageNumber
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updatePages(scrollView)
}
func updatePages(_ scrollView: UIScrollView) -> Void {
let w = scrollView.frame.size.width
guard w != 0 else { return }
let x = scrollView.contentOffset.x
if x == 0 {
// we've scrolled to the left
// move last page to position Zero
guard let pg = pages.last,
let v = pg.vc.view else { return }
// remove the last arranged subview
stack.arrangedSubviews.last?.removeFromSuperview()
// insert last "page" view as first arranged subview
stack.insertArrangedSubview(v, at: 0)
// set its width anchor
v.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
// move last page to first position in array
pages.insert(pages.removeLast(), at: 0)
} else if x == scrollView.frame.size.width * 2 {
// we've scrolled right
// move first page to last position in array
pages.append(pages.removeFirst())
// get the next page
let pg = pages[2]
guard let v = pg.vc.view else { return }
// remove first arranged subview
stack.arrangedSubviews.first?.removeFromSuperview()
// add next page view to stack
stack.addArrangedSubview(v)
// set its width anchor
v.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
}
scrollView.contentOffset.x = scrollView.frame.size.width
}
}
BasePageController 示例“页面”class:
class BasePageController: UIViewController {
let centerLabel: UILabel = {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.font = UIFont.systemFont(ofSize: 48.0, weight: .bold)
v.textAlignment = .center
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add a label at each corner
for (i, s) in ["top-left", "top-right", "bot-left", "bot-right"].enumerated() {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
v.translatesAutoresizingMaskIntoConstraints = false
v.text = s
view.addSubview(v)
let g = view.safeAreaLayoutGuide
switch i {
case 1:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
case 2:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
default:
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
}
}
centerLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(centerLabel)
NSLayoutConstraint.activate([
centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
备注:
这只是 示例代码,不应被视为“生产就绪”。
如果您只有 1 或 2 个“页面”,这将崩溃。
如果你用几十个“页面”尝试这个,你可能会遇到内存问题。
编辑 根据评论...
查看了您的项目,我发现您使用的是 UICollectionView
。
我认为问题在于您正在混合/匹配 viewModel.pages
- 具有 4
元素,而 itemsWithBoundries
- 具有 6
元素。试图调和那是相当混乱的。
所以...我将建议一种不同的、较旧的方法。
对于集合视图的 numberOfItemsInSection
,我要 return 10,000。
在 cellForItemAt
中,我将使用 indexPath.item % viewModel.pages.count
(余数/取模运算符)来 return viewModel.pages[0...3]
范围内的单元格。
在 scrollViewDidScroll
中有相同的想法 ... 获取实际的单元格项目索引 %
页数,从零到 3。
为了在两个方向上实现“无限滚动”,我将从将集合视图滚动到第 5,000 项开始(代码包括在页数不能等分为 5,000 时进行调整)。用户不太可能在任一方向滚动 5,000 页以到达“结尾”。
我用这种方法编辑了您的测试应用程序并将其发布到 GitHub:https://github.com/DonMag/Test-App 这样您就可以看到我所做的更改。