Jeder Entwickler für mobile Plattformen steht ständig vor einer Aufgabe, die nicht auf eine einzige Weise gelöst werden kann. Es gibt immer mehrere Möglichkeiten, einige schnell, einige kompliziert, und jede hat ihre eigenen Vor- und Nachteile.Das endlose / zyklische Scrollen in iOS kann nicht mit Standardwerkzeugen implementiert werden. Sie müssen verschiedene Tricks anwenden. In diesem Artikel werde ich Ihnen sagen, welche Optionen zur Lösung des Problems an der Oberfläche liegen und welche Option wir schließlich implementiert haben.Aufgabe
Es war notwendig, ein endloses zyklisches Scrollen mit Elementen in Form eines vorbereiteten Bildes, Titels und Untertitels durchzuführen. Eingabe: Das zentrale Bild wird um 16,0 Punkte vom Bildschirm eingerückt. An den Seiten des zentralen Bildes ragen „Ohren“ heraus. Der Abstand zwischen den Bannern beträgt 8,0 Punkte.Wir untersuchen, was Kollegen getan haben
Dodobanner sind nicht zyklisch, das zentrale Banner befindet sich in einem bestimmten Abstand immer neben dem linken Rand.Auto.ru - Banner sind nicht zyklisch. Wenn Sie wischen, werden die Banner sehr lange durchgeblättert.Ozon - Banner sind zyklisch, können aber nicht durch Berühren gesteuert werden: Sobald die Richtung des Wischens festgelegt ist, kann das Bild nicht mehr gestoppt werden.Wildbeeren - Banner sind nicht zyklisch, es erfolgt eine Zentrierung, lange Fertigstellung der Scroll-Animation.Letzte Wünsche:- Banner sollten zentriert sein.
- Das Scrollen sollte ohne lange Wartezeit auf die Animation enden.
- Verwaltbarkeit von Bannern: Es sollte möglich sein, die Bildlaufanimation mit dem Finger zu steuern.
Implementierungsoptionen
Wenn eine neue Aufgabe entsteht, die noch nicht gelöst wurde, lohnt es sich, vorhandene Lösungen und Ansätze in Betracht zu ziehen.UICollectionView
Wenn Sie in der Stirn agieren, können Sie zur Option des Erstellens kommen UICollectionView
. Wir machen die Anzahl der Elemente Int.max
und während der Initialisierung zeigen wir die Mitte und beim Aufrufen der Methode in dataSource
- func collectionView(UICollectionView, cellForItemAt: IndexPath) -> UICollectionViewCell
. Wir werden das entsprechende Element zurückgeben, vorausgesetzt, das Nullelement ist dies Int.max / 2
. Ein solches Monster mit einer Reihe von Funktionen UICollectionView
ist für unsere einfache Aufgabe ungeeignet.UIScrollView und (n + 2) UIView
Es gibt noch eine Option, mit der UIScrollView erstellt wird, absolut alle Banner werden darauf platziert und es wird am Anfang und Ende durch das Banner hinzugefügt. Wenn wir es bis zum Ende beendet haben, ändern wir den Versatz für den Benutzer unmerklich und kehren zum ersten Element zurück. Und wenn wir zurück scrollen, machen wir das Gegenteil. Infolgedessen werden mit einer großen Anzahl von Elementen eine Reihe von Ansichten erstellt, ohne sie wiederzuverwenden.QuelleEigenen Weg
Wir haben uns für UIScrollView + drei UIView entschieden. Diese UIView werden wiederverwendet. Zum Zeitpunkt des Bildlaufs kehren wir contentOffset
zum zentralen Banner zurück und ersetzen den Inhalt aller drei UIView. Und dann sollten Sie eine leichte Komponente erhalten, die diese Aufgabe schließt.Es besteht jedoch die Sorge, dass das Spoofing von Inhalten während des Bildlaufs für den Benutzer erkennbar ist. Dies erfahren wir bei der Implementierung.Implementierung
Vorbereiten von UIScrollView und Three UIImageView
Erstellen Sie einen Erben UIView
, platzieren Sie UIScrollView
drei darauf UIImageView
:final class BannersView: UIView {
private let scrollView = UIScrollView()
private let leftItemView = UIImageView()
private let centerItemView = UIImageView()
private let rightItemView = UIImageView()
init() {
super.init(frame: .zero)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.addSubview(self.scrollView)
self.setupScrollView()
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.forEach(self.scrollView.addSubview)
}
}
Fügen Sie die Implementierung der Methode mit der Einstellung hinzu scrollView
:Nach der Grundeinstellung können wir das Layout und die Platzierung vornehmen ImageView
:override func layoutSubviews() {
super.layoutSubviews()
self.scrollView.frame = self.bounds
let horizontalItemOffsetFromSuperView: CGFloat = 16.0
let spaceBetweenItems: CGFloat = 8.0
let itemWidth = self.frame.width - horizontalItemOffsetFromSuperView * 2
let itemHeight: CGFloat = self.scrollView.frame.height
var startX: CGFloat = 0.0
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.forEach { view in
view.frame.origin = CGPoint(x: startX, y: 0.0)
view.frame.size = CGSize(width: itemWidth, height: itemHeight)
startX += itemWidth + spaceBetweenItems
}
let viewsCount: CGFloat = 3.0
let contentWidth: CGFloat = itemWidth * viewsCount + spaceBetweenItems * (viewsCount - 1.0)
self.scrollView.contentSize = CGSize(width: contentWidth, height: self.frame.height)
}
Fügen Sie den UIImageView
Bildern, die wir von der Bildgenerierungs-Website https://placeholder.com abrufen, Folgendes hinzu : let imageURLs = ImageURLFactory.makeImageURLS()
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
Das Ergebnis der ersten vorbereitenden Schritte:Zentrieren Sie Bilder beim Scrollen
Um die Schriftrolle zu steuern, werden wir verwenden UIScrollViewDelegate
. Wir setup
setzen den Delegaten für die Methode UIScrollView
und setzen auch contentInset
das erste und das letzte Bild so, dass Einrückungen an den Seiten vorhanden sind.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0)
self.scrollView.delegate = self
Wir schaffen extension
für unsere BannersView
eine der Methoden. Die Delegate-Methode func scrollViewWillEndDragging
wird aufgerufen, wenn der Benutzer das Scrollen beendet. Diese Methode interessiert uns targetContentOffset
- dies ist die Variable, die für den endgültigen Bildlaufversatz verantwortlich ist (der Punkt, an dem der Bildlauf stoppt).
extension BannersView: UIScrollViewDelegate {
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let gap: CGFloat = self.centerItemView.frame.width / 3
let targetRightOffsetX = targetContentOffset.pointee.x + self.frame.width
if (self.rightItemView.frame.minX + gap) < targetRightOffsetX {
targetContentOffset.pointee.x = self.rightItemView.frame.midX - self.frame.midX
}
else if (self.leftItemView.frame.maxX - gap) > targetContentOffset.pointee.x {
targetContentOffset.pointee.x = self.leftItemView.frame.midX - self.frame.midX
}
else {
targetContentOffset.pointee.x = self.centerItemView.frame.midX - self.frame.midX
}
}
}
gap
- Dies ist die Entfernung, in der wir davon ausgehen, dass die Ansicht zentral ist. Wenn ein Drittel der Breite des orangefarbenen Bildes auf dem Bildschirm angezeigt wird, stellen wir den endgültigen Versatz so ein, dass sich das orangefarbene Bild in der Mitte befindet.targetRightOffsetX
- An dieser Stelle können Sie feststellen, ob die richtige Ansicht zentral ist.Das Ergebnis der Implementierung dieser Methode:Versatz beim Scrollen verwalten
Jetzt, direkt während des Bildlaufs, ändern wir contentOffset
die zentrale Ansicht in der Mitte des Bildschirms. Auf diese Weise kann der Benutzer unbemerkt eine Illusion von unendlichem Scrollen erzeugen.Fügen Sie eine Delegatmethode hinzu func scrollViewDidScroll(_ scrollView: UIScrollView)
, die aufgerufen wird, wenn sich contentOffset
y ändert UIScrollView
.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard
self.leftItemView.frame.width > 0,
self.centerItemView.frame.width > 0,
self.rightItemView.frame.width > 0
else {
return
}
let gap: CGFloat = self.centerItemView.frame.width / 3
let spacing: CGFloat = 8.0
let currentRightOffset: CGFloat = scrollView.contentOffset.x + self.frame.width + scrollView.contentInset.left
if (self.rightItemView.frame.maxX - gap) < currentRightOffset {
scrollView.contentOffset.x -= self.centerItemView.frame.width + spacing
} else if (self.leftItemView.frame.minX + gap) > scrollView.contentOffset.x {
scrollView.contentOffset.x += self.centerItemView.frame.width + spacing
}
}
gap
- Dies ist die Entfernung, aus der wir den Verschiebungsbedarf bestimmen contentOffset
. Wir berechnen den Punkt für rightItemView
:, self.rightItemView.frame.maxX — gap
nach dessen Schnittpunkt wir uns verschieben werden contentOffset
. Wenn zum Beispiel rightItemView
100,0 Punkte gescrollt werden , bleiben auf volle Anzeige , dann verschieben wir contentOffset
rückwärts, um die Breite einer Fahne, unter Berücksichtigung des Abstands zwischen den Fahnen (Abstand), so dass es centerItemView
an Ort und Stelle rightItemView
. In ähnlicher Weise tun wir für leftItemView
: Wir berechnen den Punkt, nach dessen Schnittpunkt wir uns ändern werden contentOffset
.Fügen Sie eine Methode hinzu func set(imageURLs: [URL])
, um Daten für die Anzeige außerhalb verfügbar zu machen. Dort übertragen wir einen Teil des Codes von setup
.Außerdem fügen wir eine Zeile hinzu, damit der Inhalt beim centerItemView
Festlegen sofort zentriert wird. horizontalItemOffsetFromSuperView
Wir haben es bereits verwendet layoutSubviews
, also setzen wir es in Konstanten und verwenden es erneut.func set(imageURLs: [URL]) {
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
Wir werden diese Methode draußen in aufrufen UIViewController.viewDidAppear
. Sie können auch die erste Ausrichtung nach verschieben layoutSubviews
, stellen Sie jedoch sicher, dass dies nur erfolgt, wenn Sie den Rahmen der gesamten Ansicht ändern. Um die Arbeit zu demonstrieren, verwenden wir die erste Methode: Also ... Mit einer scharfen Schriftrolle brach die Zentrierung.Tatsache ist, dass es beim starken Scrollen ignoriert wird targetContentOffset
. Lassen Sie uns erhöhen contentInset
, danach funktioniert alles richtig. Die zentrale Ansicht wird immer zentriert.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 300.0, bottom: 0.0, right: 300.0)
Wir ersetzen Inhalte
Die Aufgabe besteht darin contentOffset
, den Inhalt der Ansicht gleichzeitig im Versatz zu ersetzen. Wenn Sie nach rechts scrollen, wird das rechte Bild zentral, die Mitte wird links und das linke wird rechts. 1 - 2 - 3 | 2 - 3 - 1.Erstellen Sie der Einfachheit halber ein ViewModel:struct BannersViewModel {
let items: [URL] = ImageURLFactory.makeImageURLS()
}
Um zu überprüfen, welches Element sich jetzt in der Mitte befindet, fügen Sie BannersView
für jede Ansicht eine Variable und Variablen mit Inhalt hinzu: private var currentCenterItemIndex: Int = 0
private var viewModel: BannersViewModel?
private var leftItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
let leftIndex = items.index(before: self.currentCenterItemIndex)
return leftIndex < 0 ? items.last! : items[leftIndex]
}
private var centerItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
return items[self.currentCenterItemIndex]
}
private var rightItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
let rightIndex = items.index(after: self.currentCenterItemIndex)
return rightIndex >= items.count ? items.first! : items[rightIndex]
}
leftItemViewModel
, centerItemViewModel
, rightItemViewModel
- auf der Grundlage der currentCenterItemIndex
Rückkehr relevanter Inhalte für jede Ansicht. force unwrap
und fatal
hier verwenden wir es, weil die Anzahl der Elemente ≥ 3 ist (falls gewünscht, können Sie der Methode eine Prüfung hinzufügen set
).Fügen Sie Methoden hinzu, die bei Bedarf aufgerufen werden, um den Inhalt von Ansichten zu ändern: func nextItem() {
self.currentCenterItemIndex += 1
if self.viewModel?.items.count == self.currentCenterItemIndex {
self.currentCenterItemIndex = 0
}
self.updateViews()
}
func prevItem() {
self.currentCenterItemIndex -= 1
if self.currentCenterItemIndex == -1 {
self.currentCenterItemIndex = self.viewModel?.items.indices.last ?? 0
}
self.updateViews()
}
private func updateViews() {
self.leftItemView.setImage(with: self.leftItemViewModel)
self.centerItemView.setImage(with: self.centerItemViewModel)
self.rightItemView.setImage(with: self.rightItemViewModel)
}
Ändern Sie die Methode, die extern zum Anzeigen von Inhalten verwendet wird: func set(viewModel: BannersViewModel) {
self.viewModel = viewModel
self.updateViews()
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
Und wir werden anrufen nextItem
und prevItem
in der Methode den Delegaten beim Ändern contentOffset
: func scrollViewDidScroll(_ scrollView: UIScrollView) {
.......
if (self.rightItemView.frame.maxX - gap) < currentRightOffset {
scrollView.contentOffset.x -= self.centerItemView.frame.width + spacing
self.nextItem()
} else if (self.leftItemView.frame.minX + gap) > scrollView.contentOffset.x {
scrollView.contentOffset.x += self.centerItemView.frame.width + spacing
self.prevItem()
}
}
Erhöhen wir die Anzahl der Eingabelinks zu Bildern auf 5 (der Einfachheit halber gab es drei):Letzte Schritte
Es bleibt ein benutzerdefiniertes UIView
Bild anstelle eines einfachen Bildes. Dies ist der Titel, der Untertitel und das Bild.Erweitern ViewModel
:struct BannersViewModel {
let items: [Item]
struct Item {
let title: String
let subtitle: String
let imageUrl: URL
}
}
Und schreibe eine Banner-Implementierung:extension BannersView {
final class ItemView: UIView {
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
init() {
super.init(frame: .zero)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.addSubview(self.imageView)
self.addSubview(self.titleLabel)
self.addSubview(self.subtitleLabel)
self.imageView.contentMode = .scaleAspectFill
self.layer.masksToBounds = true
self.layer.cornerRadius = 8.0
}
func set(viewModel: BannersViewModel.Item) {
self.titleLabel.text = viewModel.title
self.subtitleLabel.text = viewModel.subtitle
self.imageView.setImage(with: viewModel.imageUrl)
}
override func layoutSubviews() {
super.layoutSubviews()
self.imageView.frame = self.bounds
self.titleLabel.frame.origin = CGPoint(x: 16.0, y: 16.0)
self.titleLabel.frame.size = CGSize(width: self.bounds.width - 32.0, height: 20.0)
self.subtitleLabel.frame.origin = CGPoint(x: 16.0, y: self.titleLabel.frame.maxY + 4.0)
self.subtitleLabel.frame.size = self.titleLabel.frame.size
}
}
}
Ersetzen UIImageView
und ViewModel
in BannersView
::
.......
private let leftItemView = ItemView()
private let centerItemView = ItemView()
private let rightItemView = ItemView()
private var leftItemViewModel: BannersViewModel.Item { ... }
private var centerItemViewModel: BannersViewModel.Item { ... }
private var rightItemViewModel: BannersViewModel.Item { ... }
.......
private func updateViews() {
self.leftItemView.set(viewModel: self.leftItemViewModel)
self.centerItemView.set(viewModel: self.centerItemViewModel)
self.rightItemView.set(viewModel: self.rightItemViewModel)
}
.......
Ergebnis:Ergebnisse
Es stellte sich als interessante Aufgabe heraus, eine Endlosschleifenrolle mit Bannern zu erstellen. Ich bin sicher, dass jeder in der Lage sein wird, seine eigenen Schlussfolgerungen zu ziehen oder Ideen aus unserer Entscheidung zu ziehen, mit nur drei wiederverwendbaren zu arbeiten UIView
.Wir waren erneut davon überzeugt, dass die zuerst in den Sinn kommenden Lösungen und die Lösungen, die Sie im Internet finden, nicht immer optimal sind. Zuerst hatten wir Angst, dass das Ersetzen von Inhalten während des Bildlaufs zu einem Problem führen würde, aber alles funktionierte reibungslos. Haben Sie keine Angst, Ihre Ansätze auszuprobieren, wenn Sie der Meinung sind, dass dies richtig ist. Denk mit deinem eigenen Kopf :).