[英]Swift - Infinite Scrolling for UIScrollView with an embedded UIStackView
Someone helped with the below code to achieve what I needed to do here:-有人帮助使用以下代码来实现我在这里需要做的事情:-
UIPageViewController - Detect scrolling halfway into the next view controller (almost working) to change button color? UIPageViewController - 检测滚动到下一个视图控制器(几乎可以正常工作)以更改按钮颜色的一半?
Whilst keeping the existing behaviour intact, I would like some assistance please to modify probably just the scrollViewDidScroll method to allow for an infinite smooth scrolling so when you reach the fourth / last item as you swipe right it will smoothly transition to the first page and likewise if you keep swiping back to the left as you reach the first and swipe left again the last item will be displayed, if you know what I mean, I tried doing it but was playing up a bit.在保持现有行为完整的同时,我需要一些帮助,请修改可能只是 scrollViewDidScroll 方法以允许无限平滑滚动,因此当您向右滑动时到达第四个/最后一个项目时,它将平滑过渡到第一页,同样如果您在到达第一个时继续向左滑动并再次向左滑动,则会显示最后一个项目,如果您知道我的意思,我尝试这样做,但有点播放。 Thank you
谢谢
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
}
}
}
}
Here is one way to do this...这是一种方法来做到这一点......
Since the scroll view has paging enabled, we are able to have only 3 views "pages" in the scroll view at a time.由于滚动视图启用了分页,因此我们一次只能在滚动视图中查看 3 个“页面”。
Assuming 4 total "pages"...假设总共有 4 个“页面”......
This approach loads all "page" view controllers as child view controllers.这种方法将所有“页面”视图控制器加载为子视图控制器。 If there we end up with, say, 20 "pages" - particularly if they are "heavy" (lots of subviews, code, etc) - we would want to load controllers only when we need to show them, and unload them when they're removed from the 3 "scroll slots."
如果我们最终有 20 个“页面”——特别是如果它们“重”(大量子视图、代码等)——我们只想在需要显示控制器时加载它们,并在它们出现时卸载它们'已从 3 个“滚动槽”中移除。
struct MyPage
Defines a page as a view controller and an index page number: struct
MyPage
定义一个页面作为视图控制器和一个索引页码:
struct MyPage {
var vc: UIViewController!
var pageNumber: Int!
}
PagedScrollViewController class: PagedScrollViewController类:
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 example "page" class: BasePageController示例“页面”类:
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),
])
}
}
Notes:笔记:
This is example code only and should not be considered "production ready."这只是示例代码,不应被视为“生产就绪”。
If you have only 1 or 2 "pages" this will crash.如果您只有 1 或 2 个“页面”,则会崩溃。
If you try this with a couple dozen "pages" you'll likely hit memory problems.如果你用几十个“页面”尝试这个,你可能会遇到内存问题。
Edit based on comments...根据评论编辑...
Took a look at your project, and I see you're using a UICollectionView
instead.看看你的项目,我看到你正在使用
UICollectionView
。
I think the issue is that you're mixing / matching your viewModel.pages
- which has 4
elements, and your itemsWithBoundries
- which has 6
elements.我认为问题在于您正在混合/匹配您的
viewModel.pages
- 它有4
元素,而您的itemsWithBoundries
- 它有6
元素。 Trying to reconcile that is pretty messy.试图调和那是相当混乱的。
So... I'm going to suggest a different, older approach.所以......我将建议一种不同的、更古老的方法。
For the collection view's numberOfItemsInSection
, I'm going to return 10,000.对于集合视图的
numberOfItemsInSection
,我将返回 10,000。
In cellForItemAt
, I'll use indexPath.item % viewModel.pages.count
(the remainder / modulo operator) to return a cell in the viewModel.pages[0...3]
range.在
cellForItemAt
,我将使用indexPath.item % viewModel.pages.count
(余数/模运算符)返回viewModel.pages[0...3]
范围内的单元格。
Same idea in scrollViewDidScroll
... get the actual cell item index %
number of pages to get Zero thru 3. scrollViewDidScroll
相同想法...获取实际单元格项目索引%
页数以从 0 到 3。
To achieve "infinite scrolling" in both directions, I'll start with scrolling the collection view to item 5,000 (code includes adjusting that if the number of pages is not equally divisible into 5,000).为了在两个方向上实现“无限滚动”,我将首先将集合视图滚动到第 5,000 项(代码包括调整,如果页数不能被等分为 5,000)。 It's pretty unlikely a user would scroll 5,000 pages in either direction to reach the "end."
用户不太可能向任一方向滚动 5,000 页以到达“终点”。
I edited your Test App with that approach and posted it to GitHub: https://github.com/DonMag/Test-App so you can see the changes I made.我用这种方法编辑了您的测试应用程序并将其发布到 GitHub: https : //github.com/DonMag/Test-App,以便您可以看到我所做的更改。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.