German (Deutsch) translation by Katharina Grigorovich-Nevolina (you can also view the original English article)
In den vorherigen Abschnitten haben wir über Stapelaktualisierungen und Stapellöschungen gesprochen. In diesem Tutorial werden wir uns genauer ansehen, wie asynchrones Abrufen implementiert wird und in welchen Situationen Ihre Anwendung von dieser neuen API profitieren kann.
1. Das Problem
Wie Batch-Updates steht auch das asynchrone Abrufen seit geraumer Zeit auf der Wunschliste vieler Entwickler. Abrufanforderungen können komplex sein und eine nicht triviale Zeit in Anspruch nehmen. Während dieser Zeit blockiert die Abrufanforderung den Thread, auf dem sie ausgeführt wird, und blockiert daher den Zugriff auf den verwalteten Objektkontext, in dem die Abrufanforderung ausgeführt wird. Das Problem ist einfach zu verstehen, aber wie sieht die Lösung von Apple aus?
2. Die Lösung
Apples Antwort auf dieses Problem ist das asynchrone Abrufen. Eine asynchrone Abrufanforderung wird im Hintergrund ausgeführt. Dies bedeutet, dass andere Aufgaben während der Ausführung nicht blockiert werden, z. B. das Aktualisieren der Benutzeroberfläche im Hauptthread.
Das asynchrone Abrufen bietet außerdem zwei weitere praktische Funktionen: Fortschrittsberichterstattung und Stornierung. Eine asynchrone Abrufanforderung kann jederzeit abgebrochen werden, beispielsweise wenn der Benutzer entscheidet, dass der Abschluss der Abrufanforderung zu lange dauert. Die Fortschrittsberichterstattung ist eine nützliche Ergänzung, um dem Benutzer den aktuellen Status der Abrufanforderung anzuzeigen.
Asynchrones Abrufen ist eine flexible API. Es ist nicht nur möglich, eine asynchrone Abrufanforderung abzubrechen, sondern auch Änderungen am Kontext des verwalteten Objekts vorzunehmen, während die asynchrone Abrufanforderung ausgeführt wird. Mit anderen Worten, der Benutzer kann Ihre Anwendung weiterhin verwenden, während die Anwendung im Hintergrund eine asynchrone Abrufanforderung ausführt.
3. Wie funktioniert es?
Wie bei Stapelaktualisierungen werden asynchrone Abrufanforderungen als NSPersistentStoreRequest
-Objekt an den Kontext des verwalteten Objekts übergeben, genauer gesagt als Instanz der NSAsynchronousFetchRequest
-Klasse.
Eine NSAsynchronousFetchRequest
-Instanz wird mit einem NSFetchRequest
-Objekt und einem Abschlussblock initialisiert. Der Abschlussblock wird ausgeführt, wenn die asynchrone Abrufanforderung ihre Abrufanforderung abgeschlossen hat.
Lassen Sie uns die Aufgabenanwendung, die wir zuvor in dieser Reihe erstellt haben, erneut betrachten und die aktuelle Implementierung der NSFetchedResultsController
-Klasse durch eine asynchrone Abrufanforderung ersetzen.
Schritt 1: Projekteinrichtung
Laden Sie das Projekt von GitHub herunter oder klonen Sie es und öffnen Sie es in Xcode 7. Bevor wir mit der NSAsynchronousFetchRequest
-Klasse arbeiten können, müssen wir einige Änderungen vornehmen. Wir können die NSFetchedResultsController
-Klasse nicht zum Verwalten der Daten der Tabellenansicht verwenden, da die NSFetchedResultsController
-Klasse für die Ausführung auf dem Hauptthread konzipiert wurde.
Schritt 2: Ersetzen des Controllers für abgerufene Ergebnisse
Aktualisieren Sie zunächst die ViewController
-Klasse wie unten gezeigt. Wir entfernen die Eigenschaft fetchedResultsController
und erstellen eine neue Eigenschaft, items
, vom Typ [Item]
zum Speichern der zu erledigenden Elemente. Dies bedeutet auch, dass die ViewController
-Klasse nicht mehr dem NSFetchedResultsControllerDelegate
-Protokoll entsprechen muss.
1 |
import UIKit |
2 |
import CoreData |
3 |
|
4 |
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { |
5 |
|
6 |
let ReuseIdentifierToDoCell = "ToDoCell" |
7 |
|
8 |
@IBOutlet weak var tableView: UITableView! |
9 |
|
10 |
var managedObjectContext: NSManagedObjectContext! |
11 |
|
12 |
var items: [NSManagedObject] = [] |
13 |
|
14 |
...
|
15 |
|
16 |
}
|
Bevor wir die viewDidLoad()
-Methode umgestalten, möchte ich zunächst die Implementierung des UITableViewDataSource
-Protokolls aktualisieren. Schauen Sie sich die Änderungen an, die ich vorgenommen habe.
1 |
func numberOfSectionsInTableView(tableView: UITableView) -> Int { |
2 |
return 1 |
3 |
}
|
1 |
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
2 |
return items.count |
3 |
}
|
1 |
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) { |
2 |
// Fetch Record
|
3 |
let record = items[indexPath.row] |
4 |
|
5 |
// Update Cell
|
6 |
if let name = record.valueForKey("name") as? String { |
7 |
cell.nameLabel.text = name |
8 |
}
|
9 |
|
10 |
if let done = record.valueForKey("done") as? Bool { |
11 |
cell.doneButton.selected = done |
12 |
}
|
13 |
|
14 |
cell.didTapButtonHandler = { |
15 |
if let done = record.valueForKey("done") as? Bool { |
16 |
record.setValue(!done, forKey: "done") |
17 |
}
|
18 |
}
|
19 |
}
|
1 |
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { |
2 |
if (editingStyle == .Delete) { |
3 |
// Fetch Record
|
4 |
let record = items[indexPath.row] |
5 |
|
6 |
// Delete Record
|
7 |
managedObjectContext.deleteObject(record) |
8 |
}
|
9 |
}
|
Wir müssen auch eine Codezeile in der Methode prepareForSegue(_:sender:)
ändern, wie unten gezeigt.
1 |
// Fetch Record
|
2 |
let record = items[indexPath.row] |
Löschen Sie zu guter Letzt die Implementierung des NSFetchedResultsControllerDelegate
-Protokolls, da wir es nicht mehr benötigen.
Schritt 3: Erstellen der asynchronen Abrufanforderung
Wie Sie unten sehen können, erstellen wir die asynchrone Abrufanforderung in der viewDidLoad()
-Methode des Ansichts-Controllers. Nehmen wir uns einen Moment Zeit, um zu sehen, was los ist.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
// Initialize Fetch Request
|
5 |
let fetchRequest = NSFetchRequest(entityName: "Item") |
6 |
|
7 |
// Add Sort Descriptors
|
8 |
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)] |
9 |
|
10 |
// Initialize Asynchronous Fetch Request
|
11 |
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in |
12 |
dispatch_async(dispatch_get_main_queue(), { () -> Void in |
13 |
self.processAsynchronousFetchResult(asynchronousFetchResult) |
14 |
})
|
15 |
}
|
16 |
|
17 |
do { |
18 |
// Execute Asynchronous Fetch Request
|
19 |
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) |
20 |
|
21 |
print(asynchronousFetchResult) |
22 |
|
23 |
} catch { |
24 |
let fetchError = error as NSError |
25 |
print("\(fetchError), \(fetchError.userInfo)") |
26 |
}
|
27 |
}
|
Zunächst erstellen und konfigurieren wir eine NSFetchRequest
-Instanz, um die asynchrone Abrufanforderung zu initialisieren. Es ist diese Abrufanforderung, die die asynchrone Abrufanforderung im Hintergrund ausführen wird.
1 |
// Initialize Fetch Request
|
2 |
let fetchRequest = NSFetchRequest(entityName: "Item") |
3 |
|
4 |
// Add Sort Descriptors
|
5 |
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)] |
Um eine NSAsynchronousFetchRequest
-Instanz zu initialisieren, rufen wir init(request:CompletionBlock:)
auf und übergeben fetchRequest
und einen Completion-Block.
1 |
// Initialize Asynchronous Fetch Request
|
2 |
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in |
3 |
dispatch_async(dispatch_get_main_queue(), { () -> Void in |
4 |
self.processAsynchronousFetchResult(asynchronousFetchResult) |
5 |
})
|
6 |
}
|
Der Abschlussblock wird aufgerufen, wenn die asynchrone Abrufanforderung die Ausführung ihrer Abrufanforderung abgeschlossen hat. Der Abschlussblock verwendet ein Argument vom Typ NSAsynchronousFetchResult
, das das Ergebnis der Abfrage sowie einen Verweis auf die ursprüngliche asynchrone Abrufanforderung enthält.
Im Abschlussblock rufen wir processAsynchronousFetchResult (_:)
auf und übergeben das NSAsynchronousFetchResult
-Objekt. Wir werden uns in wenigen Augenblicken diese Hilfsmethode ansehen.
Das Ausführen der asynchronen Abrufanforderung ist fast identisch mit dem Ausführen einer NSBatchUpdateRequest
. Wir rufen executeRequest(_:)
im Kontext des verwalteten Objekts auf und übergeben die asynchrone Abrufanforderung.
1 |
do { |
2 |
// Execute Asynchronous Fetch Request
|
3 |
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) |
4 |
|
5 |
print(asynchronousFetchResult) |
6 |
|
7 |
} catch { |
8 |
let fetchError = error as NSError |
9 |
print("\(fetchError), \(fetchError.userInfo)") |
10 |
}
|
Obwohl die asynchrone Abrufanforderung im Hintergrund ausgeführt wird, beachten Sie, dass die Methode executeRequest(_:)
sofort zurückgegeben wird und uns ein NSAsynchronousFetchResult
-Objekt übergibt. Sobald die asynchrone Abrufanforderung abgeschlossen ist, wird dasselbe NSAsynchronousFetchResult
-Objekt mit dem Ergebnis der Abrufanforderung gefüllt.
Denken Sie aus dem vorherigen Tutorial daran, dass executeRequest(_:)
eine Wurfmethode ist. Wir fangen alle Fehler in der catch
-Klausel der do-catch
-Anweisung ab und drucken sie zum Debuggen auf die Konsole.
Schritt 4: Verarbeiten des asynchronen Abrufergebnisses
Die processAsynchronousFetchResult(_:)
-Methode ist nichts anderes als eine Hilfsmethode, in der wir das Ergebnis der asynchronen Abrufanforderung verarbeiten. Wir setzen die items
-Eigenschaft des View-Controllers mit dem Inhalt der finalResult
-Eigenschaft des Ergebnisses und laden die Tabellenansicht neu.
1 |
func processAsynchronousFetchResult(asynchronousFetchResult: NSAsynchronousFetchResult) { |
2 |
if let result = asynchronousFetchResult.finalResult { |
3 |
// Update Items
|
4 |
items = result as! [NSManagedObject] |
5 |
|
6 |
// Reload Table View
|
7 |
tableView.reloadData() |
8 |
}
|
9 |
}
|
Schritt 5: Erstellen und Ausführen
Erstellen Sie das Projekt und führen Sie die Anwendung im iOS-Simulator aus. Wenn Ihre Anwendung abstürzt, wenn sie versucht, die asynchrone Abrufanforderung auszuführen, verwenden Sie möglicherweise eine API, die ab iOS 9 (und OS X El Capitan) veraltet ist.
In Core Data und Swift: Concurrency habe ich die verschiedenen Parallelitätstypen erläutert, die ein verwalteter Objektkontext haben kann. Ab iOS 9 (und OS X El Capitan) ist der ConfinementConcurrencyType
veraltet. Gleiches gilt für die init()
-Methode der NSManagedObjectContext
-Klasse, da eine Instanz mit dem Parallelitätstyp ConfinementConcurrencyType
erstellt wird.
Wenn Ihre Anwendung abstürzt, verwenden Sie höchstwahrscheinlich einen verwalteten Objektkontext mit einem ConfinementConcurrencyType
-Parallelitätstyp, der das asynchrone Abrufen nicht unterstützt. Zum Glück ist die Lösung einfach. Erstellen Sie einen verwalteten Objektkontext mit dem angegebenen Initialisierer init(concurrencyType:)
und übergeben Sie MainQueueConcurrencyType
oder PrivateQueueConcurrencyType
als Parallelitätstyp.
4. Fortschritt anzeigen
Die NSAsynchronousFetchRequest
-Klasse bietet Unterstützung für die Überwachung des Fortschritts der Abrufanforderung und es ist sogar möglich, eine asynchrone Abrufanforderung abzubrechen, wenn der Benutzer beispielsweise feststellt, dass der Abschluss zu lange dauert.
Die NSAsynchronousFetchRequest
-Klasse nutzt die NSProgress
-Klasse für die Fortschrittsberichterstattung sowie für das Abbrechen einer asynchronen Abrufanforderung. Die NSProgress
-Klasse, die seit iOS 7 und OS X Mavericks verfügbar ist, ist eine clevere Möglichkeit, den Fortschritt einer Aufgabe zu überwachen, ohne die Aufgabe eng an die Benutzeroberfläche koppeln zu müssen.
Die NSProgress
-Klasse unterstützt auch das Abbrechen. Auf diese Weise kann eine asynchrone Abrufanforderung abgebrochen werden. Lassen Sie uns herausfinden, was wir tun müssen, um die Fortschrittsberichterstattung für die asynchrone Abrufanforderung zu implementieren.
Schritt 1: Hinzufügen von SVProgressHUD
Wir zeigen dem Benutzer den Fortschritt der asynchronen Abrufanforderung mithilfe der SVProgressHUD-Bibliothek von Sam Vermette. Der einfachste Weg, dies zu erreichen, sind CocoaPods. So sieht das Podfile des Projekts aus.
1 |
source 'https://github.com/CocoaPods/Specs.git' |
2 |
platform :ios, '9.0' |
3 |
use_frameworks!
|
4 |
|
5 |
pod 'SVProgressHUD', '~> 1.1' |
Führen Sie pod install
über die Befehlszeile aus und vergessen Sie nicht, den von CocoaPods für Sie erstellten Arbeitsbereich anstelle des Xcode-Projekts zu öffnen.
Schritt 2: Einrichten von NSProgress
In diesem Artikel werden wir die NSProgress
-Klasse nicht im Detail untersuchen, aber Sie können gerne mehr darüber in der Apple-Dokumentation lesen. Wir erstellen eine NSProgress
-Instanz in der viewDidLoad()
-Methode des View Controllers, bevor wir die asynchrone Abrufanforderung ausführen.
1 |
// Create Progress
|
2 |
let progress = NSProgress(totalUnitCount: 1) |
3 |
|
4 |
// Become Current
|
5 |
progress.becomeCurrentWithPendingUnitCount(1) |
Sie werden überrascht sein, dass wir die Gesamtzahl der Einheiten auf 1
setzen. Der Grund ist einfach. Wenn Core Data die asynchrone Abrufanforderung ausführt, weiß es nicht, wie viele Datensätze im persistenten Speicher gefunden werden. Dies bedeutet auch, dass wir dem Benutzer den relativen Fortschritt nicht anzeigen können - einen Prozentsatz. Stattdessen zeigen wir dem Benutzer den absoluten Fortschritt - die Anzahl der gefundenen Datensätze.
Sie können dieses Problem beheben, indem Sie eine Abrufanforderung ausführen, um die Anzahl der Datensätze abzurufen, bevor Sie die asynchrone Abrufanforderung ausführen. Ich ziehe es jedoch vor, dies nicht zu tun, da dies auch bedeutet, dass das Abrufen der Datensätze aus dem persistenten Speicher aufgrund der zusätzlichen Abrufanforderung zu Beginn länger dauert.
Schritt 3: Hinzufügen eines Beobachters
Wenn wir die asynchrone Abrufanforderung ausführen, wird uns sofort ein NSAsynchronousFetchResult
-Objekt übergeben. Dieses Objekt verfügt über eine progress
-Eigenschaft vom Typ NSProgress
. Diese progress
-Eigenschaft müssen wir beobachten, wenn wir Fortschrittsaktualisierungen erhalten möchten.
1 |
// Create Progress
|
2 |
let progress = NSProgress(totalUnitCount: 1) |
3 |
|
4 |
// Become Current
|
5 |
progress.becomeCurrentWithPendingUnitCount(1) |
6 |
|
7 |
// Execute Asynchronous Fetch Request
|
8 |
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) as! NSAsynchronousFetchResult |
9 |
|
10 |
if let asynchronousFetchProgress = asynchronousFetchResult.progress { |
11 |
asynchronousFetchProgress.addObserver(self, forKeyPath: "completedUnitCount", options: NSKeyValueObservingOptions.New, context: nil) |
12 |
}
|
13 |
|
14 |
// Resign Current
|
15 |
progress.resignCurrent() |
Beachten Sie, dass wir resignCurrent
für das progress
-Objekt aufrufen, um den früheren Aufruf von becomeCurrentWithPendingUnitCount:
auszugleichen. Beachten Sie, dass beide Methoden im selben Thread aufgerufen werden müssen.
Schritt 4: Entfernen des Beobachters
Im Abschlussblock der asynchronen Abrufanforderung entfernen wir den Beobachter und schließen das Fortschritts-HUD ab.
1 |
// Initialize Asynchronous Fetch Request
|
2 |
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in |
3 |
dispatch_async(dispatch_get_main_queue(), { () -> Void in |
4 |
// Dismiss Progress HUD
|
5 |
SVProgressHUD.dismiss() |
6 |
|
7 |
// Process Asynchronous Fetch Result
|
8 |
self.processAsynchronousFetchResult(asynchronousFetchResult) |
9 |
|
10 |
if let asynchronousFetchProgress = asynchronousFetchResult.progress { |
11 |
// Remove Observer
|
12 |
asynchronousFetchProgress.removeObserver(self, forKeyPath: "completedUnitCount") |
13 |
}
|
14 |
})
|
15 |
}
|
Bevor wir observeValueForKeyPath (_:ofObject:change:context:)
implementieren, müssen wir den Fortschritts-HUD anzeigen, bevor wir die asynchrone Abrufanforderung erstellen.
1 |
// Show Progress HUD
|
2 |
SVProgressHUD.showWithStatus("Fetching Data", maskType: .Gradient) |
Schritt 5: Fortschrittsberichterstattung
Wir müssen nur noch die Methode observeValueForKeyPath (_:ofObject:change:context:)
implementieren. Wir prüfen, ob context
ProgressContext
entspricht, erstellen ein status
-Objekt, indem wir die Anzahl der abgeschlossenen Datensätze aus dem change
-Wörterbuch extrahieren und das Fortschritts-HUD aktualisieren. Beachten Sie, dass wir die Benutzeroberfläche im Hauptthread aktualisieren.
1 |
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { |
2 |
if keyPath == "completedUnitCount" { |
3 |
dispatch_async(dispatch_get_main_queue(), { () -> Void in |
4 |
if let changes = change, number = changes["new"] { |
5 |
// Create Status
|
6 |
let status = "Fetched \(number) Records" |
7 |
|
8 |
// Show Progress HUD
|
9 |
SVProgressHUD.setStatus(status) |
10 |
}
|
11 |
})
|
12 |
}
|
13 |
}
|
5. Dummy-Daten
Wenn wir unsere Anwendung ordnungsgemäß testen möchten, benötigen wir mehr Daten. Ich empfehle zwar nicht, den folgenden Ansatz in einer Produktionsanwendung zu verwenden, aber es ist eine schnelle und einfache Möglichkeit, die Datenbank mit Daten zu füllen.
Öffnen Sie AppDelegate.swift und aktualisieren Sie die Methode application (_:didFinishLaunchingWithOptions:)
wie unten gezeigt. Die populateDatabase()
-Methode ist eine einfache Hilfsmethode, bei der wir der Datenbank Dummy-Daten hinzufügen.
1 |
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { |
2 |
// Populate Database
|
3 |
populateDatabase() |
4 |
|
5 |
...
|
6 |
|
7 |
return true |
8 |
}
|
Die Implementierung ist unkompliziert. Da wir Dummy-Daten nur einmal einfügen möchten, überprüfen wir die Standarddatenbank des Benutzers auf den Schlüssel "didPopulateDatabase"
. Wenn der Schlüssel nicht gesetzt ist, fügen wir Dummy-Daten ein.
1 |
private func populateDatabase() { |
2 |
// Helpers
|
3 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
4 |
guard userDefaults.objectForKey("didPopulateDatabase") == nil else { return } |
5 |
|
6 |
// Create Entity
|
7 |
let entityDescription = NSEntityDescription.entityForName("Item", inManagedObjectContext: self.managedObjectContext) |
8 |
|
9 |
for index in 0...1000000 { |
10 |
// Initialize Record
|
11 |
let record = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext) |
12 |
|
13 |
// Populate Record
|
14 |
record.setValue(NSDate(), forKey: "createdAt") |
15 |
record.setValue("Item \(index)", forKey: "name") |
16 |
}
|
17 |
|
18 |
// Save Changes
|
19 |
saveManagedObjectContext() |
20 |
|
21 |
// Update User Defaults
|
22 |
userDefaults.setBool(true, forKey: "didPopulateDatabase") |
23 |
}
|
Die Anzahl der Datensätze ist wichtig. Wenn Sie die Anwendung auf dem iOS-Simulator ausführen möchten, können Sie 100.000 oder 1.000.000 Datensätze einfügen. Dies funktioniert auf einem physischen Gerät nicht so gut und dauert zu lange.
In der for
-Schleife erstellen wir ein verwaltetes Objekt und füllen es mit Daten. Beachten Sie, dass wir die Änderungen des verwalteten Objektkontexts nicht bei jeder Iteration der for
-Schleife speichern.
Schließlich aktualisieren wir die Standarddatenbank des Benutzers, um sicherzustellen, dass die Datenbank beim nächsten Start der Anwendung nicht gefüllt wird.
Toll. Führen Sie die Anwendung im iOS-Simulator aus, um das Ergebnis anzuzeigen. Sie werden feststellen, dass es einige Momente dauert, bis die asynchrone Abrufanforderung mit dem Abrufen von Datensätzen beginnt und das Fortschritts-HUD aktualisiert.



6. Änderungen brechen
Durch Ersetzen der Controller-Klasse für abgerufene Ergebnisse durch eine asynchrone Abrufanforderung haben wir einige Teile der Anwendung beschädigt. Das Tippen auf das Häkchen eines Aufgabenelements scheint beispielsweise nicht mehr zu funktionieren. Während die Datenbank aktualisiert wird, spiegelt die Benutzeroberfläche die Änderung nicht wider. Die Lösung ist ziemlich einfach zu reparieren und ich überlasse es Ihnen, eine Lösung zu implementieren. Sie sollten jetzt über genügend Wissen verfügen, um das Problem zu verstehen und eine geeignete Lösung zu finden.
Schlussfolgerung
Ich bin sicher, Sie stimmen zu, dass das asynchrone Abrufen überraschend einfach zu verwenden ist. Das schwere Heben wird von Core Data durchgeführt, was bedeutet, dass die Ergebnisse der asynchronen Abrufanforderung nicht manuell mit dem verwalteten Objektkontext zusammengeführt werden müssen. Ihre einzige Aufgabe besteht darin, die Benutzeroberfläche zu aktualisieren, wenn die asynchrone Abrufanforderung Ihnen die Ergebnisse übermittelt.