Unendliche Schriftrolle mit Bannern oder Vorgehensweise mit drei Ansichten


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.maxund 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 UICollectionViewist 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.


Quelle

Eigenen Weg


Wir haben uns für UIScrollView + drei UIView entschieden. Diese UIView werden wiederverwendet. Zum Zeitpunkt des Bildlaufs kehren wir contentOffsetzum 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 UIScrollViewdrei 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:

  • decelerationRate- Dieser Parameter gibt an, wie schnell die Bildlaufanimation verlangsamt wird. In unserem Fall ist es am besten .fast.
  • showsHorizontalScrollIndicator - Dieser Parameter ist für die Anzeige der horizontalen Bildlaufleiste verantwortlich:

    private func setupScrollView() {
        self.scrollView.decelerationRate = .fast
        self.scrollView.showsHorizontalScrollIndicator = false
    }
    

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 UIImageViewBildern, 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 setupsetzen den Delegaten für die Methode UIScrollViewund setzen auch contentInsetdas 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 extensionfür unsere BannersVieweine der Methoden. Die Delegate-Methode func scrollViewWillEndDraggingwird 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 contentOffsetdie 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 contentOffsety ä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 — gapnach dessen Schnittpunkt wir uns verschieben werden contentOffset. Wenn zum Beispiel rightItemView100,0 Punkte gescrollt werden , bleiben auf volle Anzeige , dann verschieben wir contentOffsetrückwärts, um die Breite einer Fahne, unter Berücksichtigung des Abstands zwischen den Fahnen (Abstand), so dass es centerItemViewan 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 centerItemViewFestlegen sofort zentriert wird. horizontalItemOffsetFromSuperViewWir haben es bereits verwendet layoutSubviews, also setzen wir es in Konstanten und verwenden es erneut.

func set(imageURLs: [URL]) {
    //    ImageView
    let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
    imageViews.enumerated().forEach { key, view in
        view.setImage(with: imageURLs[key])
    }
    //    ,  centerItemView   
    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 {
    //     3     
    let items: [URL] = ImageURLFactory.makeImageURLS()
}

Um zu überprüfen, welches Element sich jetzt in der Mitte befindet, fügen Sie BannersViewfü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 currentCenterItemIndexRückkehr relevanter Inhalte für jede Ansicht. force unwrapund fatalhier 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 nextItemund prevItemin 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 UIViewBild 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 UIImageViewund ViewModelin 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 :).

All Articles