Swift - Infinite Scrolling für UIScrollView mit einer eingebetteten UIStackView
Jemand hat mit dem folgenden Code geholfen, um das zu erreichen, was ich hier tun musste: -
UIPageViewController - Erkennen Sie das Scrollen auf halbem Weg zum nächsten Ansichts-Controller (funktioniert fast), um die Tastenfarbe zu ändern?
Unter Beibehaltung des vorhandenen Verhaltens möchte ich Sie dabei unterstützen, wahrscheinlich nur die scrollViewDidScroll-Methode zu ändern, um ein unendliches reibungsloses Scrollen zu ermöglichen. Wenn Sie also das vierte / letzte Element erreichen, während Sie nach rechts wischen, wechselt es reibungslos zur ersten Seite und ebenfalls Wenn Sie beim Erreichen des ersten nach links und wieder nach links wischen, wird das letzte Element angezeigt. Wenn Sie wissen, was ich meine, habe ich es versucht, aber ein bisschen gespielt. Vielen Dank
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
}
}
}
}
Antworten
Hier ist eine Möglichkeit, dies zu tun ...
Da in der Bildlaufansicht Paging aktiviert ist, können in der Bildlaufansicht jeweils nur 3 Ansichten "Seiten" angezeigt werden.
Angenommen, insgesamt 4 "Seiten" ...
- Beginnen Sie mit den Seiten 4, 1, 2
- Stellen Sie den Versatz x für den Bildlaufinhalt so ein, dass die mittlere Seite sichtbar ist
- Wenn wir zur nächsten Seite scrollen, verschieben Sie die Ansichten auf 1, 2, 3 und setzen Sie den Versatz x für den Bildlaufinhalt erneut so, dass die mittlere Seite sichtbar ist
- Wenn Sie zur vorherigen Seite scrollen, verschieben Sie die Ansichten auf 3, 4, 1 und setzen Sie den Versatz x für den Bildlaufinhalt erneut so, dass die mittlere Seite sichtbar ist
Dieser Ansatz lädt alle "Seiten" -Ansichts-Controller als untergeordnete Ansichts-Controller. Wenn wir dort beispielsweise 20 "Seiten" haben - insbesondere wenn sie "schwer" sind (viele Unteransichten, Code usw.) - möchten wir Controller nur dann laden, wenn wir sie anzeigen müssen, und sie entladen, wenn sie vorhanden sind werden aus den 3 "Scroll-Slots" entfernt.
structMyPage Definiert eine Seite als View Controller und eine Indexseitennummer:
struct MyPage {
var vc: UIViewController!
var pageNumber: Int!
}
PagedScrollViewController- Klasse:
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- Beispielklasse "page":
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),
])
}
}
Anmerkungen:
Dies ist nur Beispielcode und sollte nicht als "produktionsbereit" betrachtet werden.
Wenn Sie nur 1 oder 2 "Seiten" haben, stürzt dies ab.
Wenn Sie dies mit ein paar Dutzend "Seiten" versuchen, werden Sie wahrscheinlich auf Speicherprobleme stoßen.
Bearbeiten basierend auf Kommentaren ...
Ich habe mir Ihr Projekt angesehen und sehe, dass Sie stattdessen ein UICollectionViewverwenden.
Ich denke, das Problem ist, dass Sie Ihre viewModel.pages- die 4Elemente hat, und Ihre itemsWithBoundries- die 6Elemente haben - mischen / anpassen . Der Versuch, das in Einklang zu bringen, ist ziemlich chaotisch.
Also ... ich werde einen anderen, älteren Ansatz vorschlagen.
Für die Sammlungsansicht numberOfItemsInSectionwerde ich 10.000 zurückgeben.
In verwende cellForItemAtich indexPath.item % viewModel.pages.count(den Rest- / Modulo-Operator), um eine Zelle im viewModel.pages[0...3]Bereich zurückzugeben.
Dieselbe Idee in scrollViewDidScroll... Ermitteln Sie die tatsächliche %Anzahl der Seiten des Zellenelementindex, um Null bis 3 zu erhalten.
Um ein "unendliches Scrollen" in beide Richtungen zu erreichen, beginne ich mit dem Scrollen der Sammlungsansicht zu Element 5.000 (Code beinhaltet das Anpassen, wenn die Anzahl der Seiten nicht gleichmäßig in 5.000 teilbar ist). Es ist ziemlich unwahrscheinlich, dass ein Benutzer 5.000 Seiten in beide Richtungen scrollen würde, um das "Ende" zu erreichen.
Ich habe Ihre Test-App mit diesem Ansatz bearbeitet und auf GitHub gepostet: https://github.com/DonMag/Test-App So können Sie die Änderungen sehen, die ich vorgenommen habe.