1. Code
  2. Mobile Development
  3. iOS Development

Arbeiten mit NSURLSession: Teil 4

Im vorherigen Tutorial haben wir begonnen, einen einfachen Podcast-Client zu erstellen, um das, was wir über NSURLSession gelernt haben, in die Praxis umzusetzen. Bisher kann unser Podcast-Client die iTunes-Such-API abfragen, einen Podcast-Feed herunterladen und eine Liste der Folgen anzeigen. In diesem Tutorial vergrößern wir einen weiteren interessanten Aspekt von NSURLSession, das Herunterladen außerhalb des Prozesses. Lassen Sie mich Ihnen zeigen, wie das funktioniert.
Scroll to top
This post is part of a series called Working with NSURLSession.
Working with NSURLSession: AFNetworking 2.0

German (Deutsch) translation by Federicco Ancie (you can also view the original English article)

Im vorherigen Tutorial haben wir begonnen, einen einfachen Podcast-Client zu erstellen, um das, was wir über NSURLSession gelernt haben, in die Praxis umzusetzen. Bisher kann unser Podcast-Client die iTunes-Such-API abfragen, einen Podcast-Feed herunterladen und eine Liste der Folgen anzeigen. In diesem Tutorial vergrößern wir einen weiteren interessanten Aspekt von NSURLSession, das Herunterladen außerhalb des Prozesses. Lassen Sie mich Ihnen zeigen, wie das funktioniert.


Einführung

In diesem vierten und letzten Tutorial zu NSURLSession werden wir uns die Aufgaben außerhalb des Prozesses genauer ansehen und insbesondere Aufgaben herunterladen. Unser Podcast-Client kann bereits eine Liste von Episoden anzeigen, kann jedoch derzeit keine einzelnen Episoden herunterladen. Das wird der Schwerpunkt dieses Tutorials sein.

Hintergrund Uploads und Downloads

Das Hinzufügen von Unterstützung für Hintergrund-Uploads und -Downloads ist mit NSURLSession überraschend einfach. Apple bezeichnet sie als nicht prozessbedingte Uploads und Downloads, da die Aufgaben von einem Hintergrunddämon und nicht von Ihrer Anwendung verwaltet werden. Selbst wenn Ihre Anwendung während einer Upload- oder Download-Aufgabe abstürzt, wird die Aufgabe im Hintergrund fortgesetzt.

Überblick

Ich möchte mir einen Moment Zeit nehmen, um mir genauer anzusehen, wie Aufgaben außerhalb des Prozesses funktionieren. Es ist ziemlich einfach, wenn Sie ein vollständiges Bild des Prozesses haben. Das Aktivieren von Hintergrund-Uploads und -Downloads ist nichts anderes als das Umlegen eines Schalters in der Konfiguration Ihrer Sitzung. Mit einem ordnungsgemäß konfigurierten Sitzungsobjekt können Sie Upload- und Download-Aufgaben im Hintergrund planen.

Wenn ein Upload oder Download initiiert wird, entsteht ein Hintergrunddämon. Der Daemon kümmert sich um die Aufgabe und sendet Aktualisierungen über die in der NSURLSession-API deklarierten Delegatenprotokolle an die Anwendung. Wenn Ihre Anwendung aus irgendeinem Grund nicht mehr ausgeführt wird, wird die Aufgabe im Hintergrund fortgesetzt, da es sich um den Daemon handelt, der die Aufgabe verwaltet. Sobald die Aufgabe beendet ist, wird die Anwendung benachrichtigt, die die Aufgabe erstellt hat. Es stellt erneut eine Verbindung mit der Hintergrundsitzung her, die die Aufgabe erstellt hat, und der Daemon, der die Aufgabe verwaltet, informiert die Sitzung über den Abschluss der Aufgabe und übergibt im Fall einer Download-Aufgabe die Datei an die Sitzung. Die Sitzung ruft dann die entsprechenden Delegierungsmethoden auf, um sicherzustellen, dass Ihre Anwendung die entsprechenden Aktionen ausführen kann, z. B. das Verschieben der Datei an einen dauerhafteren Speicherort. Das ist genug Theorie für jetzt. Mal sehen, was wir tun müssen, um Out-of-Process-Downloads in Singlecast zu implementieren.


1. Unterklasse UITableViewCell

Schritt 1: Aktualisieren Sie das Haupt-Storyboard

Im Moment verwenden wir Prototypzellen, um die Tabellenansicht zu füllen. Um uns ein bisschen mehr Flexibilität zu geben, müssen wir eine UITableViewCell-Unterklasse erstellen. Öffnen Sie das Haupt-Storyboard, wählen Sie die Tabellenansicht der MTViewController-Instanz aus und setzen Sie die Anzahl der Prototypzellen auf 0.

Update the project's main storyboard.Update the project's main storyboard.Update the project's main storyboard.

Schritt 2: Unterklasse erstellen

Öffnen Sie das Menü Datei von Xcode und wählen Sie Neu > Datei.... Erstellen Sie eine neue Objective-C-Klasse, nennen Sie sie MTEpisodeCell und stellen Sie sicher, dass sie von UITableViewCell erbt. Teilen Sie Xcode mit, wo Sie die Klassendateien speichern möchten, und klicken Sie auf Erstellen.

Create a subclass of UITableViewCell.Create a subclass of UITableViewCell.Create a subclass of UITableViewCell.

Schritt 3: Aktualisieren Sie die Klassenschnittstelle

Die Oberfläche von MTEpisodeCell ist einfach, wie Sie im folgenden Codeausschnitt sehen können. Wir deklarieren lediglich einen Eigenschaft progress vom Typ float. Wir werden dies verwenden, um den Fortschritt der Download-Aufgabe zu aktualisieren und anzuzeigen, die wir zum Herunterladen einer Episode verwenden.

1
#import <UIKit/UIKit.h>

2
3
@interface MTEpisodeCell : UITableViewCell
4
5
@property (assign, nonatomic) float progress;
6
7
@end

Schritt 4: Implementierung der Klasse

Die Implementierung von MTEpisodeCell ist etwas komplizierter, aber nicht kompliziert. Anstatt eine Instanz von UIProgressView zu verwenden, füllen wir die Inhaltsansicht der Zelle mit einer Volltonfarbe, um den Fortschritt der Download-Aufgabe anzuzeigen. Dazu fügen wir der Inhaltsansicht der Zelle eine Unteransicht hinzu und aktualisieren ihre Breite, wenn sich die progress eigenschaft der Zelle ändert. Beginnen Sie mit der Deklaration einer progressiView für private Eigenschaften vom Typ UIView.

1
#import "MTEpisodeCell.h"

2
3
@interface MTEpisodeCell ()
4
5
@property (strong, nonatomic) UIView *progressView;
6
7
@end

Wir überschreiben den von der Klasse festgelegten Initialisierer wie unten gezeigt. Beachten Sie, wie wir das Argument style ignorieren und UITableViewCellStyleSubtitle an den festgelegten Initialisierer der Oberklasse übergeben. Dies ist wichtig, da die Tabellenansicht UITableViewCellStyleDefault als Stil der Zelle übergibt, wenn wir nach einer neuen Zelle fragen.

Im Initialisierer setzen wir die Hintergrundfarbe der Text- und Detailtextbeschriftungen auf [UIColor clearColor] und erstellen die Fortschrittsansicht. Zwei Details sind besonders wichtig. Zuerst fügen wir die Fortschrittsansicht als Unteransicht der Inhaltsansicht der Zelle bei Index 0 ein, um sicherzustellen, dass sie unter den Textbeschriftungen eingefügt wird. Zweitens rufen wir updateView auf, um sicherzustellen, dass der Rahmen der Fortschrittsansicht aktualisiert wird, um den Wert des progress widerzuspiegeln, der während der Initialisierung der Zelle auf 0 gesetzt wird.

1
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
2
    self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
3
4
    if (self) {
5
        // Helpers

6
        CGSize size = self.contentView.bounds.size;
7
8
        // Configure Labels

9
        [self.textLabel setBackgroundColor:[UIColor clearColor]];
10
        [self.detailTextLabel setBackgroundColor:[UIColor clearColor]];
11
12
        // Initialize Progress View

13
        self.progressView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, size.width, size.height)];
14
15
        // Configure Progress View

16
        [self.progressView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleWidth)];
17
        [self.progressView setBackgroundColor:[UIColor colorWithRed:0.678 green:0.886 blue:0.557 alpha:1.0]];
18
        [self.contentView insertSubview:self.progressView atIndex:0];
19
20
        // Update View

21
        [self updateView];
22
    }
23
24
    return self;
25
}

Bevor wir uns die Implementierung von updateView ansehen, müssen wir die Setter-Methode der progress-Eigenschaft überschreiben. Die einzige Änderung, die wir an der Standardimplementierung von setProgress vornehmen, ist der Aufruf von updateView, wenn die Instanzvariable _progress aktualisiert wird. Dadurch wird sichergestellt, dass die Fortschrittsansicht jedes Mal aktualisiert wird, wenn die progress eigenschaft der Zelle aktualisiert wird.

1
- (void)setProgress:(CGFloat)progress {
2
    if (_progress != progress) {
3
        _progress = progress;
4
5
        // Update View

6
        [self updateView];
7
    }
8
}

In updateView berechnen wir die neue Breite der Fortschrittsansicht basierend auf dem Wert der progress eigenschaft der Zelle.

1
- (void)updateView {
2
    // Helpers

3
    CGSize size = self.contentView.bounds.size;
4
5
    // Update Frame Progress View

6
    CGRect frame = self.progressView.frame;
7
    frame.size.width = size.width * self.progress;
8
    self.progressView.frame = frame;
9
}

Schritt 5: Verwenden Sie MTEpisodeCell

Um die MTEpisodeCell nutzen zu können, müssen wir einige Änderungen in der MTViewController-Klasse vornehmen. Fügen Sie zunächst eine Importanweisung für MTEpisodeCell hinzu.

1
#import "MTViewController.h"

2
3
#import "MWFeedParser.h"

4
#import "SVProgressHUD.h"

5
#import "MTEpisodeCell.h"

6
7
@interface MTViewController () <MWFeedParserDelegate>
8
9
@property (strong, nonatomic) NSDictionary *podcast;
10
@property (strong, nonatomic) NSMutableArray *episodes;
11
@property (strong, nonatomic) MWFeedParser *feedParser;
12
13
@end

Rufen Sie in der viewDidLoad-Methode des View-Controllers setupView auf, eine Hilfsmethode, die wir als Nächstes implementieren.

1
- (void)viewDidLoad {
2
    [super viewDidLoad];
3
4
    // Setup View

5
    [self setupView];
6
7
    // Load Podcast

8
    [self loadPodcast];
9
10
    // Add Observer

11
    [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"MTPodcast" options:NSKeyValueObservingOptionNew context:NULL];
12
}

In setupView rufen wir setupTableView auf, eine weitere Hilfsmethode, mit der wir der Tabellenansicht mitteilen, dass sie die MTEpisodeCell-Klasse verwenden soll, wenn sie eine Zelle mit der Wiederverwendungskennung von EpisodeCell benötigt.

1
- (void)setupView {
2
    // Setup Table View

3
    [self setupTableView];
4
}
1
- (void)setupTableView {
2
    // Register Class for Cell Reuse

3
    [self.tableView registerClass:[MTEpisodeCell class] forCellReuseIdentifier:EpisodeCell];
4
}

Bevor wir das Projekt erstellen und die Anwendung ausführen, müssen wir unsere Implementierung von tableView:cellForRowAtIndexPath: wie unten gezeigt aktualisieren.

1
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
2
    MTEpisodeCell *cell = (MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier:EpisodeCell forIndexPath:indexPath];
3
4
    // Fetch Feed Item

5
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
6
7
    // Configure Table View Cell

8
    [cell.textLabel setText:feedItem.title];
9
    [cell.detailTextLabel setText:[NSString stringWithFormat:@"%@", feedItem.date]];
10
11
    return cell;
12
}

Schritt 6: Erstellen und ausführen

Führen Sie Ihre Anwendung im iOS-Simulator oder auf einem Testgerät aus, um das Ergebnis anzuzeigen. Wenn sich nichts geändert hat, haben Sie die Schritte korrekt ausgeführt. Bisher haben wir lediglich die Prototypzellen durch Instanzen von MTEpisodeCell ersetzt.


2. Erstellen Sie eine Hintergrundsitzung

Um Out-of-Process-Downloads zu ermöglichen, benötigen wir eine Sitzung, die so konfiguriert ist, dass sie Out-of-Process-Downloads unterstützt. Dies ist mit der NSURLSession-API überraschend einfach. Es gibt allerdings ein paar Fallstricke.

Schritt 1: session eigenschaft erstellen

Deklarieren Sie zunächst eine neue session-Eigenschaft vom Typ NSURLSession in der MTViewController-Klasse und stellen Sie sicher, dass die Klasse den Protokollen NSURLSessionDelegate und NSURLSessionDownloadDelegate entspricht.

1
#import "MTViewController.h"

2
3
#import "MWFeedParser.h"

4
#import "SVProgressHUD.h"

5
#import "MTEpisodeCell.h"

6
7
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate, MWFeedParserDelegate>
8
9
@property (strong, nonatomic) NSDictionary *podcast;
10
@property (strong, nonatomic) NSMutableArray *episodes;
11
@property (strong, nonatomic) MWFeedParser *feedParser;
12
13
@property (strong, nonatomic) NSURLSession *session;
14
15
@end

In viewDidLoad legen wir die session-Eigenschaft fest, indem wir backgroundSession auf der View Controller-Instanz aufrufen. Dies ist eine der Fallstricke, über die ich gesprochen habe.

1
- (void)viewDidLoad {
2
    [super viewDidLoad];
3
4
    // Setup View

5
    [self setupView];
6
7
    // Initialize Session

8
    [self setSession:[self backgroundSession]];
9
10
    // Load Podcast

11
    [self loadPodcast];
12
13
    // Add Observer

14
    [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"MTPodcast" options:NSKeyValueObservingOptionNew context:NULL];
15
}

Werfen wir einen Blick auf die Implementierung von backgroundSession. In backgroundSession deklarieren wir statisch eine session-Variable und verwenden dispatch_once (Grand Central Dispatch), um die Hintergrundsitzung zu instanziieren. Dies ist zwar nicht unbedingt erforderlich, unterstreicht jedoch die Tatsache, dass wir jeweils nur eine Hintergrundsitzung benötigen. Dies ist eine bewährte Methode, die auch in der WWDC-Sitzung zur NSURLSession-API erwähnt wird.

Im Block dispatch_once erstellen wir zunächst ein NSURLSessionConfiguration-Objekt, indem wir backgroundSessionConfiguration: aufrufen und eine Zeichenfolge als Bezeichner übergeben. Der Bezeichner, den wir übergeben, identifiziert die Hintergrundsitzung eindeutig. Dies ist der Schlüssel, wie wir später sehen werden. Anschließend erstellen wir eine Sitzungsinstanz, indem wir sessionWithConfiguration:delegate:delegateQueue: aufrufen und das Sitzungskonfigurationsobjekt übergeben, die delegate-Eigenschaft der Sitzung festlegen und nil als drittes Argument übergeben.

1
- (NSURLSession *)backgroundSession {
2
    static NSURLSession *session = nil;
3
    static dispatch_once_t onceToken;
4
    dispatch_once(&onceToken, ^{
5
        // Session Configuration

6
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.mobiletuts.Singlecast.BackgroundSession"];
7
8
        // Initialize Session

9
        session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
10
    });
11
12
    return session;
13
}
Durch Übergeben von nil als drittes Argument von sessionWithConfiguration:delegate:delegateQueue: erstellt die Sitzung eine Warteschlange für serielle Operationen für uns. Diese Operationswarteschlange wird zum Ausführen der Aufrufe der Delegatenmethode und der Aufrufe des Abschlusshandlers verwendet.

3. Episode herunterladen

Schritt 1: Download-Aufgabe erstellen

Es ist Zeit, die von uns erstellte Hintergrundsitzung zu nutzen und die MTEpisodeCell zu verwenden. Beginnen wir mit der Implementierung von tableView: didSelectRowAtIndexPath:, einer Methode des UITableViewDelegate-Protokolls. Die Implementierung ist unkompliziert, wie Sie unten sehen können. Wir rufen die richtige MWFeedItem-Instanz aus dem episodes-Array ab und übergeben sie an downloadEpisodeWithFeedItem:.

1
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
2
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
3
4
    // Fetch Feed Item

5
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
6
7
    // Download Episode with Feed Item

8
    [self downloadEpisodeWithFeedItem:feedItem];
9
}

In downloadEpisodeWithFeedItem: extrahieren wir die Remote-URL aus dem Feed-Element, indem wir urlForFeedItem: aufrufen. Erstellen Sie eine Download-Aufgabe, indem Sie downloadTaskWithURL: in der Hintergrundsitzung aufrufen, und senden Sie eine Nachricht mit dem resume, um die Download-Aufgabe zu starten.

1
- (void)downloadEpisodeWithFeedItem:(MWFeedItem *)feedItem {
2
    // Extract URL for Feed Item

3
    NSURL *URL = [self urlForFeedItem:feedItem];
4
5
    if (URL) {
6
        // Schedule Download Task

7
        [[self.session downloadTaskWithURL:URL] resume];
8
    }
9
}

Wie Sie vielleicht erraten haben, ist urlForFeedItem: eine bequeme Methode, die wir verwenden. Wir werden es in diesem Projekt noch einige Male verwenden. Wir erhalten einen Verweis auf das enclosures-Array des Feed-Elements, extrahieren das erste Gehäuse und ziehen das Objekt für den url-Schlüssel heraus. Wir erstellen eine NSURL-Instanz und geben sie zurück.

1
- (NSURL *)urlForFeedItem:(MWFeedItem *)feedItem {
2
    NSURL *result = nil;
3
4
    // Extract Enclosures

5
    NSArray *enclosures = [feedItem enclosures];
6
    if (!enclosures || !enclosures.count) return result;
7
8
    NSDictionary *enclosure = [enclosures objectAtIndex:0];
9
    NSString *urlString = [enclosure objectForKey:@"url"];
10
    result = [NSURL URLWithString:urlString];
11
12
    return result;
13
}

Wir sind noch nicht fertig. Gibt Ihnen der Compiler drei Warnungen? Dies ist nicht überraschend, da wir die erforderlichen Methoden der Protokolle NSURLSessionDelegate und NSURLSessionDownloadDelegate noch nicht implementiert haben. Wir müssen diese Methoden auch implementieren, wenn wir den Fortschritt der Download-Aufgaben anzeigen möchten.

Schritt 2: Implementieren von Protokollen

Die erste Methode, die wir implementieren müssen, ist URLSession:downloadTask:didResumeAtOffset:. Diese Methode wird aufgerufen, wenn eine Download-Aufgabe fortgesetzt wird. Da dies in diesem Lernprogramm nicht behandelt wird, protokollieren wir einfach eine Nachricht in der Xcode-Konsole.

1
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
2
    NSLog(@"%s", __PRETTY_FUNCTION__);
3
}

Interessanter ist die Implementierung von URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:. Diese Methode wird jedes Mal aufgerufen, wenn einige Bytes von der Sitzung heruntergeladen wurden. Bei dieser Delegatmethode berechnen wir den Fortschritt, rufen die richtige Zelle ab und aktualisieren die Fortschrittseigenschaft der Zelle, wodurch wiederum die Fortschrittsansicht der Zelle aktualisiert wird. Haben Sie den Aufruf dispatch_async entdeckt? Es gibt keine Garantie dafür, dass die Delegate-Methode im Hauptthread aufgerufen wird. Da wir die Benutzeroberfläche aktualisieren, indem wir den Fortschritt der Zelle festlegen, müssen wir die progress-Eigenschaft der Zelle im Hauptthread aktualisieren.

1
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
2
    // Calculate Progress

3
    double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
4
5
    // Update Table View Cell

6
    MTEpisodeCell *cell = [self cellForForDownloadTask:downloadTask];
7
8
    dispatch_async(dispatch_get_main_queue(), ^{
9
        [cell setProgress:progress];
10
    });
11
}

Die Implementierung von cellForForDownloadTask: ist unkompliziert. Wir ziehen die Remote-URL mithilfe ihrer originalRequest-Eigenschaft aus der Download-Aufgabe und durchlaufen die Feed-Elemente im episodes-Array, bis eine Übereinstimmung vorliegt. Wenn wir eine Übereinstimmung gefunden haben, fragen wir die Tabellenansicht nach der entsprechenden Zelle und geben sie zurück.

1
- (MTEpisodeCell *)cellForForDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
2
    // Helpers

3
    MTEpisodeCell *cell = nil;
4
    NSURL *URL = [[downloadTask originalRequest] URL];
5
6
    for (MWFeedItem *feedItem in self.episodes) {
7
        NSURL *feedItemURL = [self urlForFeedItem:feedItem];
8
9
        if ([URL isEqual:feedItemURL]) {
10
            NSUInteger index = [self.episodes indexOfObject:feedItem];
11
            cell = (MTEpisodeCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
12
            break;
13
        }
14
    }
15
16
    return cell;
17
}

Die dritte Delegatmethode des NSURLSessionDownloadDelegate-Protokolls, die wir implementieren müssen, ist URLSession:downloadTask:didFinishDownloadingToURL:. Wie bereits in den vorherigen Tutorials erwähnt, besteht einer der Vorteile der NSURLSession-API darin, dass Downloads sofort auf die Festplatte geschrieben werden. Das Ergebnis ist, dass uns eine lokale URL in URLSession:downloadTask:didFinishDownloadingToURL: übergeben wird. Die lokale URL, die wir erhalten, verweist jedoch auf eine temporäre Datei. Es liegt in unserer Verantwortung, die Datei an einen dauerhafteren Speicherort zu verschieben, und genau das tun wir in URLSession:downloadTask:didFinishDownloadingToURL:.

1
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
2
    // Write File to Disk

3
    [self moveFileWithURL:location downloadTask:downloadTask];
4
}

In moveFileWithURL:downloadTask: extrahieren wir den Dateinamen der Episode aus der Download-Aufgabe und erstellen eine URL im Verzeichnis Documents der Anwendung, indem wir URLForEpisodeWithName: aufrufen. Wenn die temporäre Datei, die wir von der Hintergrundsitzung erhalten haben, auf eine gültige Datei verweist, verschieben wir diese Datei in ihre neue Heimat im Verzeichnis "Dokumente" der Anwendung.

1
- (void)moveFileWithURL:(NSURL *)URL downloadTask:(NSURLSessionDownloadTask *)downloadTask {
2
    // Filename

3
    NSString *fileName = [[[downloadTask originalRequest] URL] lastPathComponent];
4
5
    // Local URL

6
    NSURL *localURL = [self URLForEpisodeWithName:fileName];
7
8
    NSFileManager *fm = [NSFileManager defaultManager];
9
10
    if ([fm fileExistsAtPath:[URL path]]) {
11
        NSError *error = nil;
12
        [fm moveItemAtURL:URL toURL:localURL error:&error];
13
14
        if (error) {
15
            NSLog(@"Unable to move temporary file to destination. %@, %@", error, error.userInfo);
16
        }
17
    }
18
}
Ich verwende in meinen iOS-Projekten viele Hilfsmethoden, da dies zu DRY-Code führt. Es ist auch eine gute Praxis, Methoden zu erstellen, die nur eines tun. Das Testen wird auf diese Weise viel einfacher.

URLForEpisodeWithName: ist eine weitere Hilfsmethode, die episodesDirectory aufruft. In URLForEpisodeWithName: hängen wir das Argument name an das Episodes-Verzeichnis an, das sich im Documents-Verzeichnis der Anwendung befindet.

1
- (NSURL *)URLForEpisodeWithName:(NSString *)name {
2
    if (!name) return nil;
3
    return [self.episodesDirectory URLByAppendingPathComponent:name];
4
}

In episodesDirectory erstellen wir die URL für das Episodenverzeichnis und erstellen das Verzeichnis, falls es noch nicht vorhanden ist.

1
- (NSURL *)episodesDirectory {
2
    NSURL *documents = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
3
    NSURL *episodes = [documents URLByAppendingPathComponent:@"Episodes"];
4
5
    NSFileManager *fm = [NSFileManager defaultManager];
6
7
    if (![fm fileExistsAtPath:[episodes path]]) {
8
        NSError *error = nil;
9
        [fm createDirectoryAtURL:episodes withIntermediateDirectories:YES attributes:nil error:&error];
10
11
        if (error) {
12
            NSLog(@"Unable to create episodes directory. %@, %@", error, error.userInfo);
13
        }
14
    }
15
16
    return episodes;
17
}

Schritt 3: Erstellen und ausführen

Führen Sie die Anwendung aus und testen Sie das Ergebnis, indem Sie eine Episode aus der Liste der Episoden herunterladen. Sie sollten sehen, wie der Fortschritt der Tabellenansichtszelle von links nach rechts den Fortschritt der Download-Aufgabe widerspiegelt. Es gibt jedoch einige Probleme. Haben Sie versucht, durch die Tabellenansicht zu scrollen? Das sieht nicht richtig aus. Lassen Sie uns das beheben.


4. Erstellen Sie einen Fortschrittspuffer

Da in der Tabellenansicht Zellen so oft wie möglich wiederverwendet werden, müssen wir sicherstellen, dass jede Zelle den Download-Status der Episode, die sie darstellt, korrekt wiedergibt. Wir können dies auf verschiedene Arten beheben. Ein Ansatz besteht darin, ein Objekt zu verwenden, das den Fortschritt jeder Download-Aufgabe verfolgt, einschließlich der bereits abgeschlossenen Download-Aufgaben.

Schritt 1: Deklarieren Sie eine Eigenschaft

Beginnen wir damit, einen neuen progressBuffer für private Eigenschaften vom Typ NSMutableDictionary in der MTViewController-Klasse zu deklarieren.

1
#import "MTViewController.h"

2
3
#import "MWFeedParser.h"

4
#import "SVProgressHUD.h"

5
#import "MTEpisodeCell.h"

6
7
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate, MWFeedParserDelegate>
8
9
@property (strong, nonatomic) NSDictionary *podcast;
10
@property (strong, nonatomic) NSMutableArray *episodes;
11
@property (strong, nonatomic) MWFeedParser *feedParser;
12
13
@property (strong, nonatomic) NSURLSession *session;
14
@property (strong, nonatomic) NSMutableDictionary *progressBuffer;
15
16
@end

Schritt 2: Puffer initialisieren

In viewDidLoad initialisieren wir den Fortschrittspuffer wie unten gezeigt.

1
- (void)viewDidLoad {
2
    [super viewDidLoad];
3
4
    // Setup View

5
    [self setupView];
6
7
    // Initialize Session

8
    [self setSession:[self backgroundSession]];
9
10
    // Initialize Progress Buffer

11
    [self setProgressBuffer:[NSMutableDictionary dictionary]];
12
13
    // Load Podcast

14
    [self loadPodcast];
15
16
    // Add Observer

17
    [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"MTPodcast" options:NSKeyValueObservingOptionNew context:NULL];
18
}

Schritt 3: Aktualisieren der Tabellenansichtszellen

Der Schlüssel, den wir im Wörterbuch verwenden, ist die Remote-URL des entsprechenden Feed-Elements. In diesem Sinne können wir die tableView:cellForRowAtIndexPath: -Methode wie unten gezeigt aktualisieren. Wir ziehen die Remote-URL aus dem Feed-Element und fragen progressBuffer nach dem Wert für den Schlüssel, der der Remote-URL entspricht. Wenn der Wert nicht nil ist, setzen wir die progress eigenschaft der Zelle auf diesen Wert, andernfalls setzen wir die progress eigenschaft der Zelle auf 0.0, wodurch die Fortschrittsansicht ausgeblendet wird, indem ihre Breite auf 0.0 gesetzt wird.

1
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
2
    MTEpisodeCell *cell = (MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier:EpisodeCell forIndexPath:indexPath];
3
4
    // Fetch Feed Item

5
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
6
    NSURL *URL = [self urlForFeedItem:feedItem];
7
8
    // Configure Table View Cell

9
    [cell.textLabel setText:feedItem.title];
10
    [cell.detailTextLabel setText:[NSString stringWithFormat:@"%@", feedItem.date]];
11
12
    NSNumber *progress = [self.progressBuffer objectForKey:[URL absoluteString]];
13
    if (!progress) progress = @(0.0);
14
15
    [cell setProgress:[progress floatValue]];
16
17
    return cell;
18
}

Schritt 4: Vermeiden Sie Duplikate

Wir können den Fortschrittspuffer auch verwenden, um zu verhindern, dass Benutzer dieselbe Episode zweimal herunterladen. Schauen Sie sich die aktualisierte Implementierung von tableView:didSelectRowAtIndexPath:. Wir machen die gleichen Schritte wie in tableView:cellForRowAtIndexPath: um den Fortschrittswert aus dem Fortschrittspuffer zu extrahieren. Erst wenn der Fortschrittswert nil ist, laden wir die Episode herunter.

1
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
2
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
3
4
    // Fetch Feed Item

5
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
6
7
    // URL for Feed Item

8
    NSURL *URL = [self urlForFeedItem:feedItem];
9
10
    if (![self.progressBuffer objectForKey:[URL absoluteString]]) {
11
        // Download Episode with Feed Item

12
        [self downloadEpisodeWithFeedItem:feedItem];
13
    }
14
}

Schritt 5: Puffer aktualisieren

Der Fortschrittspuffer funktioniert in seiner aktuellen Implementierung nur, wenn wir ihn auf dem neuesten Stand halten. Dies bedeutet, dass wir auch die URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:-Methode aktualisieren müssen. Wir speichern lediglich den neuen Fortschrittswert im Fortschrittspuffer.

1
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
2
    // Calculate Progress

3
    double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
4
5
    // Update Progress Buffer

6
    NSURL *URL = [[downloadTask originalRequest] URL];
7
    [self.progressBuffer setObject:@(progress) forKey:[URL absoluteString]];
8
9
    // Update Table View Cell

10
    MTEpisodeCell *cell = [self cellForForDownloadTask:downloadTask];
11
12
    dispatch_async(dispatch_get_main_queue(), ^{
13
        [cell setProgress:progress];
14
    });
15
}

In downloadEpisodeWithFeedItem: setzen wir den Fortschrittswert beim Start der Download-Aufgabe auf 0.0.

1
- (void)downloadEpisodeWithFeedItem:(MWFeedItem *)feedItem {
2
    // Extract URL for Feed Item

3
    NSURL *URL = [self urlForFeedItem:feedItem];
4
5
    if (URL) {
6
        // Schedule Download Task

7
        [[self.session downloadTaskWithURL:URL] resume];
8
9
        // Update Progress Buffer

10
        [self.progressBuffer setObject:@(0.0) forKey:[URL absoluteString]];
11
    }
12
}

Der Sitzungsdelegierte wird benachrichtigt, wenn eine Download-Aufgabe abgeschlossen ist. In URLSession:downloadTask:didFinishDownloadingToURL: setzen wir den Fortschrittswert auf 1.0.

1
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
2
    // Write File to Disk

3
    [self moveFileWithURL:location downloadTask:downloadTask];
4
5
    // Update Progress Buffer

6
    NSURL *URL = [[downloadTask originalRequest] URL];
7
    [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]];
8
}

Schritt 6: Puffer wiederherstellen

Im Moment wird der Fortschrittspuffer nur im Speicher gespeichert, was bedeutet, dass er zwischen den Starts der Anwendung gelöscht wird. Wir könnten den Inhalt auf die Festplatte schreiben, aber um diese Anwendung einfach zu halten, werden wir den Puffer wiederherstellen oder neu erstellen, indem wir überprüfen, welche Episoden bereits heruntergeladen wurden. Die feedParser:didParseFeedItem:-Methode, die Teil des MWFeedParserDelegate-Protokolls ist, wird für jedes Element im Feed aufgerufen. Bei dieser Methode ziehen wir die Remote-URL aus dem Feed-Element, erstellen die entsprechende lokale URL und prüfen, ob die Datei vorhanden ist. Wenn dies der Fall ist, setzen wir den entsprechenden Fortschrittswert für dieses Feedelement auf 1.0, um anzuzeigen, dass es bereits heruntergeladen wurde.

1
- (void)feedParser:(MWFeedParser *)parser didParseFeedItem:(MWFeedItem *)item {
2
    if (!self.episodes) {
3
        self.episodes = [NSMutableArray array];
4
    }
5
6
    [self.episodes addObject:item];
7
8
    // Update Progress Buffer

9
    NSURL *URL = [self urlForFeedItem:item];
10
    NSURL *localURL = [self URLForEpisodeWithName:[URL lastPathComponent]];
11
12
    if ([[NSFileManager defaultManager] fileExistsAtPath:[localURL path]]) {
13
        [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]];
14
    }
15
}

Schritt 7: Spülen und wiederholen

Führen Sie die Anwendung noch einmal aus, um festzustellen, ob die Probleme mit der Tabellenansicht behoben sind. Die Anwendung sollte sich nun auch merken, welche Folgen bereits heruntergeladen wurden.


5. Ein guter Bürger sein

Es ist wichtig, dass unsere Anwendung ein guter Bürger ist, indem sie nicht mehr CPU-Zyklen verschwendet oder mehr Batteriestrom verbraucht als benötigt. Was bedeutet das für unseren Podcast-Client? Wenn eine Download-Aufgabe von unserer Anwendung gestartet wird und die Anwendung in den Hintergrund wechselt, benachrichtigt der Hintergrund-Daemon, der die Download-Aufgabe unserer Anwendung verwaltet, unsere Anwendung über die Hintergrundsitzung, dass die Download-Aufgabe beendet wurde. Bei Bedarf startet der Hintergrunddämon unsere Anwendung, damit er auf diese Benachrichtigungen reagieren und die heruntergeladene Datei verarbeiten kann.

In unserem Beispiel müssen wir nichts Besonderes tun, um sicherzustellen, dass unsere Anwendung wieder eine Verbindung zur ursprünglichen Hintergrundsitzung herstellt. Dies wird von der MTViewController-Instanz erledigt. Wir müssen das Betriebssystem jedoch benachrichtigen, wenn unsere Anwendung die Verarbeitung der Downloads abgeschlossen hat, indem wir einen Hintergrundabschluss-Handler aufrufen.

Wenn unsere Anwendung vom Betriebssystem geweckt wird, um auf die Benachrichtigungen der Hintergrundsitzung zu antworten, wird dem Anwendungsdelegierten eine Nachricht der gesendet application:handleEventsForBackgroundURLSession:finishHandler:. Bei dieser Methode können wir bei Bedarf erneut eine Verbindung zur Hintergrundsitzung herstellen und den an uns übergebenen Abschlusshandler aufrufen. Durch das Aufrufen des Completion-Handlers weiß das Betriebssystem, dass unsere Anwendung nicht mehr im Hintergrund ausgeführt werden muss. Dies ist wichtig für die Optimierung der Batterielebensdauer. Wie machen wir das in der Praxis?

Schritt 1: Deklarieren Sie eine Eigenschaft

Wir müssen zuerst eine Eigenschaft für die MTAppDelegate-Klasse deklarieren, um einen Verweis auf den Completion-Handler beizubehalten, den wir von der erhalten application:handleEventsForBackgroundURLSession:CompletionHandler:. Die Immobilie muss öffentlich sein. Der Grund dafür wird gleich klar.

1
#import <UIKit/UIKit.h>

2
3
@interface MTAppDelegate : UIResponder <UIApplicationDelegate>
4
5
@property (strong, nonatomic) UIWindow *window;
6
@property (copy, nonatomic) void (^backgroundSessionCompletionHandler)();
7
8
@end

Schritt 2: Rückruf implementieren

In der application:handleEventsForBackgroundURLSession:finishHandler: speichern wir den Completion-Handler in backgroundSessionCompletionHandler, das wir vor einem Moment deklariert haben.

1
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
2
    [self setBackgroundSessionCompletionHandler:completionHandler];
3
}

Schritt 3: Rufen Sie den Handler für die Hintergrundvervollständigung auf

In der MTViewController-Klasse fügen wir zunächst eine Importanweisung für die MTAppDelegate-Klasse hinzu.

1
#import "MTViewController.h"

2
3
#import "MWFeedParser.h"

4
#import "MTAppDelegate.h"

5
#import "SVProgressHUD.h"

6
#import "MTEpisodeCell.h"

7
8
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate, MWFeedParserDelegate>
9
10
@property (strong, nonatomic) NSDictionary *podcast;
11
@property (strong, nonatomic) NSMutableArray *episodes;
12
@property (strong, nonatomic) MWFeedParser *feedParser;
13
14
@property (strong, nonatomic) NSURLSession *session;
15
@property (strong, nonatomic) NSMutableDictionary *progressBuffer;
16
17
@end

Anschließend implementieren wir eine andere Hilfsmethode, invokeBackgroundSessionCompletionHandler, die den Hintergrundabschluss-Handler aufruft, der in der backgroundSessionCompletionHandler-Eigenschaft des Anwendungsdelegierten gespeichert ist. Bei dieser Methode fragen wir die Hintergrundsitzung nach allen laufenden Aufgaben. Wenn keine Aufgaben ausgeführt werden, erhalten wir einen Verweis auf den Hintergrundabschluss-Handler des Anwendungsdelegierten. Wenn dies nicht nil ist, rufen wir ihn auf und setzen ihn auf nil.

1
- (void)invokeBackgroundSessionCompletionHandler {
2
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
3
        NSUInteger count = [dataTasks count] + [uploadTasks count] + [downloadTasks count];
4
5
        if (!count) {
6
            MTAppDelegate *applicationDelegate = (MTAppDelegate *)[[UIApplication sharedApplication] delegate];
7
            void (^backgroundSessionCompletionHandler)() = [applicationDelegate backgroundSessionCompletionHandler];
8
9
            if (backgroundSessionCompletionHandler) {
10
                [applicationDelegate setBackgroundSessionCompletionHandler:nil];
11
                backgroundSessionCompletionHandler();
12
            }
13
        }
14
    }];
15
}

Warte eine Minute. Wann rufen wir invokeBackgroundSessionCompletionHandler auf? Wir tun dies jedes Mal, wenn eine Download-Aufgabe abgeschlossen ist. Mit anderen Worten, wir rufen diese Methode in URLSession:downloadTask:didFinishDownloadingToURL: wie unten gezeigt.

1
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
2
    // Write File to Disk

3
    [self moveFileWithURL:location downloadTask:downloadTask];
4
5
    // Update Progress Buffer

6
    NSURL *URL = [[downloadTask originalRequest] URL];
7
    [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]];
8
9
    // Invoke Background Completion Handler

10
    [self invokeBackgroundSessionCompletionHandler];
11
}

6. Einpacken

Ich hoffe, Sie stimmen zu, dass unser Podcast-Client noch nicht für den App Store bereit ist, da eine der wichtigsten Funktionen, das Abspielen von Podcasts, noch fehlt. Wie ich im vorherigen Tutorial erwähnt habe, lag der Schwerpunkt dieses Projekts nicht auf der Erstellung eines Podcast-Clients mit vollem Funktionsumfang. Das Ziel dieses Projekts war es, zu veranschaulichen, wie die NSURLSession-API genutzt werden kann, um die iTunes-Such-API zu durchsuchen und Podcast-Episoden mithilfe von Daten- bzw. Out-of-Process-Download-Aufgaben herunterzuladen. Sie sollten nun ein grundlegendes Verständnis der NSURLSession-API sowie der Aufgaben außerhalb des Prozesses haben.


Abschluss

Durch die Erstellung eines einfachen Podcast-Clients haben wir uns die Daten und Download-Aufgaben genau angesehen. Wir haben auch gelernt, wie einfach es ist, Download-Aufgaben im Hintergrund zu planen. Die NSURLSession-API ist sowohl für iOS als auch für OS X ein wichtiger Schritt nach vorne. Ich empfehle Ihnen, diese benutzerfreundliche und flexible Klassensuite zu nutzen. In der letzten Folge dieser Serie werde ich mir AFNetworking 2.0 ansehen. Warum ist es ein Meilenstein? Wann sollten Sie es verwenden? Und wie ist der Vergleich mit der NSURLSession-API?