20. März 2018

Daten von URLs laden & Hintergrundverarbeitung mit OperationQueue

Main-Thread

Der Main-Thread einer iOS-App ist dafür zuständig, alle Ereignisse, wie Touch- und Zeichenevents, zu behandeln und sollte daher niemals mit länger dauernden Operationen, wie zum Beispiel Warten auf I/O oder Netzwerkzugriffe oder aufwendigen Berechnungen, blockiert werden. Solche Operationen sollten über Queues im Hintergrund, außerhalb des Main-Thread, ausgeführt werden.

Hintergrundverarbeitung

Mit einer OperationQueue können Operationen im Hintergrund abgearbeitet werden. So kann beispielsweise eine langlaufende Berechnung in den Hintergrund verlagert werden:

let backgroundQueue = OperationQueue()
backgroundQueue.addOperation {
    let stats = self.calculate()
}

Soll das Ergebnis einer solchen Hintergrundoperation im UI angezeigt werden, ist darauf zu achten, dass mit UI-Objekten nur vom Main Thread aus gearbeitet werden darf, da es ansonsten zu undefiniertem Verhalten kommt. Über OperationQueue.main können Operationen zur Abarbeitung durch die Main Queue eingestellt werden:

OperationQueue.main.addOperation {
    label.text = stats.description()
}

Diese beiden Operationen werden häufig miteinander verknüpft: im Hintergrund werden Daten beschafft oder berechnet - stehen diese zur Verfügung, stellt die Hintergrundoperation eine Operation in die Main Queue ein, um die Benutzeroberfläche zu aktualisieren:

backgroundQueue.addOperation {
    let stats = self.calculate()
    OperationQueue.main.addOperation {
        label.text = stats.description()
    }
}

Hinweis: In vielen Code-Beispielen werden die Grand Central Dispatch-Funktionen dispatch_async() und dispatch_get_main_queue() verwendet. Auch wenn mit diesen Funktionen das gleiche Ergebnis erzielt werden kann, empfiehlt es sich, stattdessen die OperationQueue-Klasse aus dem Foundation-Framework zu verwenden.

Abhängigkeiten zwischen Operationen

Dabei ist insbesondere die Möglichkeit praktisch, Abhängigkeiten zwischen den Operationen zu definieren, so dass Operationen auf das Ergebnis von anderen Operationen warten:

let op1 = BlockOperation() {
    NSLog("Background operation 1")
}
let op2 = BlockOperation() {
    NSLog("Background operation 2")
}

op2.addDependency(op1)

let queue = OperationQueue()
queue.addOperation(op1)
queue.addOperation(op2)

Daten asynchron laden mit URLSession

Mit der Klasse URLSession können im Hintergrund Daten von URLs geladen werden. Stehen die Daten bereit, wird ein completionHandler-Block ausgeführt, der die geladenen Daten übergeben bekommt. Hierbei ist darauf zu achten, dass der Aufruf des completionHandler-Blockes auf der Hintergrund-Queue erfolgt, die die Daten heruntergeladen hat:

let url = URL(string:"https://www.example.org/")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    guard error == nil else {
        NSLog("%@", "Error: \(error)");
        return
    }

    NSLog("Loaded %i bytes", data!.count)
}

task.resume()

Tutorial 1: Daten asynchron im Hintergrund laden

  1. Laden Sie den Start-Stand von dem Flashcards-Beispielprojekt: Flashcards.zip.

  2. Setzen Sie einen Haltepunkt auf den Data(contentsOf:)-Aufruf in DemoFlashcardsAPIClient:

    Haltepunkt NSData

  3. Öffnen Sie in der App die Download-Liste, so dass Sie in den Haltepunkt laufen und überzeugen Sie sich im Debug Navigator davon, dass dieser Code auf dem Main Thread ausgeführt wird und damit die Ursache für die in dieser Zeit blockierende Benutzeroberfläche ist:

    Main Thread im Xcode-Debugger
  4. Legen Sie im Project Navigator eine neue Gruppe Helper für allgemeingültige Helper-Klassen an.

  5. Laden Sie die Klasse RESTResource.swift herunter und kopieren Sie sie in diese Gruppe. Achten Sie dabei darauf, dass die Option Copy items if needed aktiviert ist:

    Import File Copy Items
  6. Passen Sie die Eigenschaft allSets im Protokoll und in der Implementierung so an, dass eine RESTResource statt den Daten zurückgegeben wird:

    protocol FlashcardsAPIClient {
    
        var allSets : RESTResource<SetList> { get }
        // ...
    
    }
    
    
    class DemoFlashcardsAPIClient : FlashcardsAPIClient {
    
        var allSets: RESTResource<SetList> {
        let url = URL(string: "https://www.ralfebert.de/flashcards/sets.json")!
        return RESTResource(url: url)
    }
        
        // ...
        
    }
  7. Beobachten Sie im DownloadsTableViewController die Resource mittels addValueObserver:

    override func viewDidLoad() {
        super.viewDidLoad()
        let resource = backend.allSets
    resource.addValueObserver(owner: self) { (result) in
        self.allSets = result.sets
        self.tableView.reloadData()
    }
    }
  8. Testen Sie die App.

  9. Implementieren Sie analog den Download der Kartenstapel über die RESTResource-Klasse.

  10. Benennen Sie die Klasse DemoFlashcardsAPIClient in URLFlashcardsAPIClient um.

  11. Starten Sie die App und testen Sie den Download. Committen Sie Ihre Änderungen: „18.1. Daten asynchron im Hintergrund laden“.