Swift: Fading View Pager Tutorial

Du lernst im Tutorial wie Du das unten abgebildete ScrollView realisieren kannst. Danach kannst Du die Bilder austauschen und die Komponente für eigene Projekte verwenden.

Für dieses Tutorial benötigst Du keine Kenntnisse über XCode oder Swift. Alle notwendigen Schritte werden im Tutorial beschrieben.

Das vollständige Projekt findest Du auf GitHub

Die fertige App

Abbildung 1: Die fertige App

Vorraussetzungen

Bevor Du mit dem Tutorial beginnen kannst benötigst Du XCode 6.3 oder höher, dieses Starterprojekt und einen Mac.

Note: Solltest du bereits XCode 7 verwenden, so kann es an einigen Stellen zu Fehlern kommen, da XCode 7 mit Swift 2.0 arbeitet. XCode hilft bei der nötigen Anpassung

Projekt Öffnen

Entpacke das heruntergeladene Archiv z.B. in dein Documents Verzeichnis und öffne die Datei PagingScrollview.xcodeproj. XCode öffnet jetzt das vorbereitete Projekt.

Blättern mit dem ScrollView

Ein ScrollView dient dazu einen Inhalt darzustellen, der größer ist als der zur Verfügung stehende Platz. Es gibt dem Benutzer die Möglichkeit über den Inhalt zu scrollen. Dabei ist horizontales oder vertikales scrollen möglich, zusätzlich können die beiden Varianten auch kombiniert werden. Beispielsweise um eine Zeitungsseite auf einem Handy Display darstellen zu können.

Der ScrollView wurde für Dich bereits vorbereitet. Um die Funktionsweise des ScrollViews zu demonstrieren, wirst Du diesen mit zwei simplen SubViews bestücken, um zwischen diesen hin und her blättern zu können.

Ersetze den Kommentar //code here in der Klasse ViewController mit dem folgenden Code.



    var tabCount = 2

    override func viewDidLoad() {
        super.viewDidLoad()

        var colors = [UIColor.blueColor(), UIColor.purpleColor()]

        let frame = CGRect(x: 0, y: 0, width: self.view.bounds.width*CGFloat(tabCount), height: self.view.bounds.height)
        let rootView = UIView(frame: frame)

        for index in 0..<tabCount {
            let subFrame = CGRect(x: self.view.bounds.width*CGFloat(index), y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
            let subView = UIView(frame: subFrame)

            subView.backgroundColor = colors[index]

            rootView.addSubview(subView)
        }

        // initializing Control Elements
        pageControl.numberOfPages = tabCount
        pageControl.currentPage = 0

        scrollView.delegate = self
        scrollView.addSubview(rootView)
        scrollView.contentSize = frame.size
    }
                
Starte die App und blättere zwischen den beiden Farben Blau und Lila hin und her. Wenn Du den View während eines Blättervorganges abbrichst, wirst Du durch den ScrollView automatisch entweder auf die nächste, oder die letzte Seite weiter bewegt. So eingestellt, erlaubt es der ScrollView nicht zwischen 2 Seiten stehen zu bleiben.

Einfügen der Bilder

Als nächstes fügst Du die Bilder ein. Das sind die Hintergrundbilder, sowie mehrere Bilder eines iPhones welche als Inhalt in den ScrollView eingefügt werden. Entferne dazu nun die Variable color und ersetze den Loop durch den folgenden Code:


for index in 0..<tabCount {
    let subFrame = CGRect(x: self.view.bounds.width*CGFloat(index), y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
    let subView = UIView(frame: subFrame)

    let image = UIImage(named: "iphone")
    let imageView = UIImageView(image: image)

    //Code hier

    subView.addSubview(imageView)
    rootView.addSubview(subView)
}
            

Dein Code sollte jetzt so aussehen:


import UIKit

class ViewController: UIViewController, UIScrollViewDelegate {

    @IBOutlet var scrollView: UIScrollView!
    @IBOutlet var pageControl: UIPageControl!

    let tabCount = 2

    override func viewDidLoad() {
        super.viewDidLoad()

        let frame = CGRect(x: 0, y: 0, width: self.view.bounds.width*CGFloat(tabCount), height: self.view.bounds.height)
        let rootView = UIView(frame: frame)

        for index in 0..<tabCount {
            let subFrame = CGRect(x: self.view.bounds.width*CGFloat(index), y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
            let subView = UIView(frame: subFrame)

            let image = UIImage(named: "iphone")
            let imageView = UIImageView(image: image)

            //Code hier

            subView.addSubview(imageView)
            rootView.addSubview(subView)
        }

        // initializing Control Elements
        pageControl.numberOfPages = tabCount
        pageControl.currentPage = 0

        scrollView.delegate = self
        scrollView.addSubview(rootView)
        scrollView.contentSize = frame.size
    }
}
            

Starte die App. Und swipe einmal hin und her. Dabei wird Dir auffallen, dass das Bild nicht zentriert ist.

Nicht zentriertes iPhone

Abbildung 2: Nicht zentriertes iPhone

Füge den nachfolgenden Code an der Stelle des viewDidLoad-Callsbacks ein, an der //Code hier steht. Wenn Du vorher schon mit Optionals gearbeitet hast, dürfte Dir hier eine Neuerung auffallen, es ist durch Swift 1.2 möglich mehrere Optionals innerhalb eines Blocks zu entpacken.


// zentrieren des Inhalts
if let width = image?.size.width, height = image?.size.height {
    imageView.frame.origin.x = (self.view.bounds.width-width)/2
    imageView.frame.origin.y = (self.view.bounds.height-height)/2
}
// Hintergründe erzeugen
            

State die App erneut. Du wirst sehen, dass das iPhone nun zentriert ist.

Zentriertes iPhone

Abbildung 3: Zentriertes iPhone

Was an dieser Stelle noch fehlt, ist der Hintergrund. Ein neuer Hintergrund soll immer dann langsam eingeblendet werden, wenn Du auf eine neue Seite blätterst. Jeder View in iOS besitzt das Property backgroundView. Darin kann ein Hintergrund platziert werden, welcher sich in einem UIView befindet. Jetzt stehst Du vor dem Problem, dass Du nur ein Hintergrundbild verwenden kannst, Du aber minimum 2 Bilder brauchst, um hin und her faden zu können.

Um diese Einschränkung zu umgehen nutzt Du mehrere, gestapelte, ImageViews. Die Anzahl der Tabs soll dynamisch sein, dies bedeutet dass diese ImageViews ebenfalls dynamisch im Code erzeugt werden müssen. Darum kümmerst Du Dich im folgenden Abschnitt.

Erzeugen der Hintergründe

Als Vorbereitung auf die kommenden Bilder, musst Du eine Datenstruktur anlegen, in der Du deine Bilder verwalten kannst. Dazu fügst Du den folgenden Code zu den Feldern der Klasse ViewController hinzu.


var images:[UIImageView]! = []
            

Die Schleife innerhalb von viewDidLoad erzeugt so viele Tabs, wie durch tabCount festgelegt wurde. Aktuell wird die App 2 Pages erzeugen und diese im ScrollView darstellen. Später erweiterst Du den Controller so, dass er auch in der Lage ist mehr als nur zwei Pages darzustellen.
Ersetze den Kommentar // Hintergründe erzeugen durch den folgenden Code:


// Erzeugen der einzelnen Hintergründe
let backgroundimage = UIImage(named: "page\(index+1)")
let backgroundImageView = UIImageView(frame: self.view.frame)
backgroundImageView.contentMode = .ScaleAspectFill
backgroundImageView.image = backgroundimage
images.append(backgroundImageView)


if index == 0 {
    self.view.insertSubview(backgroundImageView, belowSubview: scrollView)
} else {
    self.view.insertSubview(backgroundImageView, belowSubview: images[index-1])
}
            

Der Code erzeugt ein Hintergrundbild aus den im Ausgangsprojekt mitgelieferten Bildern. Die Namen der einzelnen Bilder sind zufälliger Weise ;) generisch. Deshalb kann bei jeder Iteration ein neues Bild, mit fortlaufender Zahl erstellt werden. Da Du auf dieses im Code erzeugte ImageView nur aufwändig Constraints setzen kannst, wählst Du eine andere Möglichkeit den View immer genau so groß zu machen, wie auch der Hintergrund, bzw der Parent-View. Die Größe des Parents bekommst Du durch den Frame des zum ViewController gehörenden Views.
Das nächste Hindernis, welches es zu überwinden gilt, ist die unterschiedliche Größe der einzelnen Bilder. Hier hilft Dir iOS. Auf den ImageView setzt Du die Option ScaleAspectFill. Dies sorgt dafür, dass die Bilder auf die entsprechende Größe gestreckt werden, ohne das Seitenverhätniss zu verlieren.

Die fade-Animation ist abhängig von der aktuellen Position des ScrollViews, diese wird durch einen Offset angegeben. Dieser Offset ist in unserem Fall immer positiv und bezieht sich auf die Distanz, die der Inhalt des ScrollViews bereits nach links geschoben wurde. An dieser Stelle ist der komplette Offset allerdings uninteressant. Du interessierst Dich an dieser Stelle nur dafür, wie viel Prozent der neuen Seite bereits sichtbar sind. Dazu schreibst Du dir eine kleine Hilfsfunktion, die Du am Ende der Klasse ViewController.swift hinzufügst.


// MARK: - Util Methods

func blätterFortschritt() -> CGFloat {
    return (scrollView.contentOffset.x % self.view.frame.width) / self.view.frame.width
}
            

Die Funktion blätterFortschritt gibt Auskunft darüber wie weit der aktuelle Umblättervorgang fortgschritten ist. Anhand dieses Fortschritts kannst Du den Alpha-Wert der einzelnen ImageViews bestimmen.
Die Animation selbst ist simpel, denn die Berechnung dazu findet bereits in der Methode blätterFortschritt() statt. Was übrig bleibt ist den berechneten Wert zu setzen, und dafür zu sorgen, dass durch die Modulo Operation kein ungewolltes Verhalten ensteht. Zum Beispiel das plötzliche Wiedereinblenden des ersten Hintergrunds, sobald der Wechsel auf die zweite Seite abgeschlossen ist. Kopiere nun den folgenden Code in die Klasse ViewController.swift:


func scrollViewDidScroll(scrollView: UIScrollView) {
    if blätterFortschritt() == 0 {
        return
    }

    images[0].alpha = 1-blätterFortschritt()
    images[1].alpha = blätterFortschritt()
}
            

Dieses Callback wird durch den ScrollView immer wieder aufgerufen, während Du scrollst. Die Anzahl der Aufrufe ist nicht beschränkt. Um so länger Du scrollst, um so öfter erfolgt der Aufruf.

Starte die App an dieser Stelle und beobachte was passiert, wenn du hin und her swipest.

Vorschau mit Hintergrund

Abbildung 4: Vorschau mit Hintergrund

Bei jeder Bewegung des ScrollView wird dadurch eine Animation, bzw. eine Reaktion des Hintergrunds ausgelöst. Du hast 2 Tabs zwischen denen Du dich hin und her bewegen kannst, und der Hintergrund wechselt entsprechend. Vielleicht fällt Dir an dieser Stelle auf, dass sich die weißen Punkte am unteren Rand des Bildes (siehe Abbildung5) nicht an die aktuelle Position des ScrollViews anpassen.

Page-Control auf Homescreen

Abbildung 5: Page-Control auf Homescreen

Die Aktualisierung dieses Controls gehst Du als nächstes an. Zuvor kopierst Du aber die beiden Util-Funktionen unter die Methode blätterFortschritt.


func bildschirmBreite() -> Int {
    return Int(self.view.frame.width)
}

func aktuelleSeite() -> Int {
    return Int(scrollView.contentOffset.x) / bildschirmBreite()
}
            

Die Erweiterung des scrollViewDidScroll-Callbacks ist simpel. Du ergänzt zu Beginn des Callbacks die folgende Zeile:


pageControl.currentPage = aktuelleSeite()
            

Jetzt zeigt der weiße Punkt unten an, auf welcher Seite Du dich befindest, genau so wie es auch auf dem Home-Screen der Fall ist. Du kannst zwischen zwei Seiten hin und her blättern und der Hintergrund passt sich entsprechend an. Aktuell blendest Du den einen aus, während der nächste Hintergrund eingeblendet wird.

Starte die App und probiere es aus. Achte dabei darauf, wie der weiße Punkt wandert.

Die zusätzlichen Bilder werden angezeigt, sobald Du den tabCount auf 7 erhöhst. Setze nun den tabCount auf 7 und starte die App.


let tabCount = 7
            

Dir fällt sicher auf, dass sobald Du anfängst zu blättern, der erste Hintergrund ruckartig angezeigt wird, und nun langsam auf den Zweiten wechselt. Blätterst Du nach Links, so wird sich die App so verhalten, als hättest Du gerade von der zweiten auf die erste Seite geblättert, unabhängig davon auf welcher Seite Du dich jetzt befindest.
Dies ist ein Resultat der Funktion animiereWechsel diese ist nicht dafür ausgelegt, mit mehr als zwei Seiten zu arbeiten. Es gibt mehr als eine Art dieses Problem zu lösen, aber ein einfacher Ansatz ist es den aktuellen Zustand für jeden einzelnen Hintergrund, zu jeder Zeit der Animation, zu bestimmen.

Um dies für jeden View bei jedem Aufruf von animiereWechsel durchführen zu können benötigst Du eine Schleife über alle Views und dazu den Index, an dem sich der View befindet. An dieser Stelle werden einige wohl einen Loop im Java Stil verwenden wollen. Aber Swift bietet hier eine sehr komfortable Art, durch das Array zu iterieren, und dabei nicht aus dem Blick zu verlieren an welcher Position man sich gerade befindet.
Dazu ersetzt Du die Methode scrollViewDidScroll durch die folgende Implementation:



func scrollViewDidScroll(scrollView: UIScrollView) {
    pageControl.currentPage = aktuelleSeite()

    let viewWidth = self.view.frame.width
    for (index, view) in enumerate(images) {
        view.alpha = (scrollView.contentOffset.x-(CGFloat(index-1)*viewWidth))/viewWidth
    }
}

            

Sobald Du den Code einegfügt hast, starte die App und suche nach Veränderungen.
Es wird permanent die erste Seite angezeigt, aber warum? - Ganz einfach, die neue Version der Funktion entfernt alte Bilder nicht mehr, sie blendet immer neue Bilder ein. Im Hintergrund existiert ein Stapel aus ImageViews, jeder einzele beinhaltet ein Bild. Dabei muss das älteste Bild, also das des ersten Tabs ganz unten liegen, da es sonst nicht vom zweiten Bild verdeckt werden kann. Dies erreichst Du, indem Du die folgenden Zeilen in der Funktion viewDidLoad:


if index == 0 {
    self.view.insertSubview(backgroundImageView, belowSubview: scrollView)
} else {
    self.view.insertSubview(backgroundImageView, belowSubview: images[index-1])
}
            

Durch diese ersetzt:


backgroundImageView.alpha = (0 == index) ? 1 : 0
self.view.insertSubview(backgroundImageView, belowSubview: scrollView)
            

Jetzt hast du dieses Tutorial erfolgreich abgeschlossen. Starte die App, und betrachte das fertige Ergebnis.

Fazit

In diesem Tutorial hast Du gelernt wie Du Views so miteinander verbinden kannst, dass diese aufeinander reagieren und dass es mit nur wenigen Zeilen Code möglich ist einen ansprechenden Effekt zu erzielen. Mit diesem Ambiente im Hintergrund lässt sich Beispielsweise das vergleichsweise langweilige Durchblättern einer Anleitung ansprechend inszenieren.

Danksagung

Für die einzelnen Hintergrundbilder der Tabs, möchten wir uns bei den folgenden Flickr Nutzern bedanken: