Статьи

Работа с NSURLSession: часть 4

В предыдущем уроке мы начали создавать простой клиент подкаста, чтобы NSURLSession на практике то, что мы узнали о NSURLSession . Пока что наш клиент подкастов может запрашивать API-интерфейс поиска iTunes, загружать канал подкастов и отображать список эпизодов. В этом уроке мы рассмотрим еще один интересный аспект NSURLSession вне процесса. Позвольте мне показать вам, как это работает.


В этом четвертом и последнем уроке о NSURLSession мы более подробно рассмотрим задачи, не связанные с процессом, в частности задачи загрузки. Наш клиент подкаста уже может показать список эпизодов, но в настоящее время ему не хватает возможности загружать отдельные эпизоды. Это будет основной темой этого урока.

Добавление поддержки фоновых загрузок и загрузок на удивление легко с NSURLSession . Apple называет их загрузками вне процесса, так как задачи управляются фоновым демоном, а не вашим приложением. Даже если ваше приложение вылетает во время задачи загрузки или выгрузки, эта задача остается в фоновом режиме.

Я хотел бы уделить несколько минут, чтобы поближе взглянуть на то, как работают внепроцессные задачи. Это довольно просто, когда у вас есть полная картина процесса. Включение фоновых загрузок и загрузок — это не что иное, как переключение переключателя в конфигурации вашего сеанса. С правильно настроенным объектом сеанса вы готовы планировать загрузку и загрузку задач в фоновом режиме.

Когда начинается загрузка или загрузка, появляется фоновый демон. Демон выполняет задачу и отправляет обновления в приложение через протоколы делегатов, объявленные в NSURLSession API. Если ваше приложение по какой-то причине перестает работать, задача продолжает работать в фоновом режиме, поскольку это демон, управляющий задачей. В момент завершения задачи приложение, создавшее задачу, получает уведомление. Он повторно подключается к фоновому сеансу, который создал задачу, и демон, управляющий задачей, информирует сеанс о завершении задачи и, в случае задачи загрузки, передает файл сеансу. Затем сеанс вызывает соответствующие методы делегата, чтобы убедиться, что ваше приложение может предпринять соответствующие действия, такие как перемещение файла в более постоянное место. Этого достаточно для теории. Давайте посмотрим, что нам нужно сделать для реализации загрузок вне процесса в Singlecast.


На данный момент мы используем прототип ячейки для заполнения табличного представления. Чтобы дать нам немного большей гибкости, нам нужно создать подкласс UITableViewCell . Откройте основную раскадровку, выберите табличное представление экземпляра MTViewController и установите количество ячеек прототипа MTViewController 0 .

Обновите основную раскадровку проекта.

Откройте меню Файл XCode и выберите New> File …. Создайте новый класс Objective-C, назовите его MTEpisodeCell и убедитесь, что он наследуется от UITableViewCell . Скажите Xcode, где вы хотите хранить файлы классов, и нажмите « Создать» .

Создайте подкласс UITableViewCell.

Интерфейс MTEpisodeCell прост, как вы можете видеть из фрагмента кода ниже. Все что мы делаем — объявляем свойство progress типа float . Мы будем использовать это для обновления и отображения хода выполнения задачи загрузки, которую мы будем использовать для загрузки эпизода.

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>
 
@interface MTEpisodeCell : UITableViewCell
 
@property (assign, nonatomic) float progress;
 
@end

Реализация MTEpisodeCell немного сложнее, но это не сложно. Вместо использования экземпляра UIProgressView , мы заполним представление содержимого ячейки сплошным цветом, чтобы показать ход выполнения задачи загрузки. Мы делаем это, добавляя подпредставление к представлению содержимого ячейки и обновляя его ширину всякий раз, когда изменяется свойство progress ячейки. Начните с объявления частного свойства progressView типа UIView .

1
2
3
4
5
6
7
#import «MTEpisodeCell.h»
 
@interface MTEpisodeCell ()
 
@property (strong, nonatomic) UIView *progressView;
 
@end

Мы переопределяем инициализатор класса, как показано ниже. Обратите внимание, как мы игнорируем аргумент style и передаем UITableViewCellStyleSubtitle назначенному инициализатору суперкласса. Это важно, потому что табличное представление будет передавать UITableViewCellStyleDefault в качестве стиля ячейки, когда мы запрашиваем новую ячейку.

В инициализаторе мы устанавливаем цвет фона для текстовых и подробных текстовых меток на [UIColor clearColor] и создаем представление хода выполнения. Две детали особенно важны. Сначала мы вставляем представление хода выполнения как подпредставление представления содержимого ячейки с индексом 0 чтобы убедиться, что оно вставлено ниже текстовых меток. Во-вторых, мы вызываем updateView чтобы удостовериться, что кадр представления прогресса обновляется, чтобы отразить значение progress , которое устанавливается в 0 во время инициализации ячейки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
— (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
 
    if (self) {
        // Helpers
        CGSize size = self.contentView.bounds.size;
 
        // Configure Labels
        [self.textLabel setBackgroundColor:[UIColor clearColor]];
        [self.detailTextLabel setBackgroundColor:[UIColor clearColor]];
 
        // Initialize Progress View
        self.progressView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, size.width, size.height)];
 
        // Configure Progress View
        [self.progressView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleWidth)];
        [self.progressView setBackgroundColor:[UIColor colorWithRed:0.678 green:0.886 blue:0.557 alpha:1.0]];
        [self.contentView insertSubview:self.progressView atIndex:0];
 
        // Update View
        [self updateView];
    }
 
    return self;
}

Прежде чем мы взглянем на реализацию updateView , нам нужно переопределить метод setter свойства progress . Единственное изменение, которое мы вносим в реализацию setProgress: по setProgress: вызывать updateView при _progress переменной экземпляра _progress . Это гарантирует, что представление хода выполнения обновляется всякий раз, когда мы обновляем свойство progress ячейки.

1
2
3
4
5
6
7
8
— (void)setProgress:(CGFloat)progress {
    if (_progress != progress) {
        _progress = progress;
 
        // Update View
        [self updateView];
    }
}

В updateView мы рассчитываем новую ширину представления прогресса на основе значения свойства progress ячейки.

1
2
3
4
5
6
7
8
9
— (void)updateView {
    // Helpers
    CGSize size = self.contentView.bounds.size;
 
    // Update Frame Progress View
    CGRect frame = self.progressView.frame;
    frame.size.width = size.width * self.progress;
    self.progressView.frame = frame;
}

Чтобы использовать MTEpisodeCell , нам нужно внести несколько изменений в класс MTViewController . Начните с добавления оператора импорта для MTEpisodeCell .

01
02
03
04
05
06
07
08
09
10
11
12
13
#import «MTViewController.h»
 
#import «MWFeedParser.h»
#import «SVProgressHUD.h»
#import «MTEpisodeCell.h»
 
@interface MTViewController () <MWFeedParserDelegate>
 
@property (strong, nonatomic) NSDictionary *podcast;
@property (strong, nonatomic) NSMutableArray *episodes;
@property (strong, nonatomic) MWFeedParser *feedParser;
 
@end

В методе viewDidLoad контроллера viewDidLoad вызовите setupView , вспомогательный метод, который мы реализуем далее.

01
02
03
04
05
06
07
08
09
10
11
12
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Setup View
    [self setupView];
 
    // Load Podcast
    [self loadPodcast];
 
    // Add Observer
    [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@»MTPodcast» options:NSKeyValueObservingOptionNew context:NULL];
}

В setupView мы вызываем setupTableView , еще один вспомогательный метод, в котором мы сообщаем табличному представлению использовать класс MTEpisodeCell всякий раз, когда ему нужна ячейка с идентификатором повторного использования EpisodeCell .

1
2
3
4
— (void)setupView {
    // Setup Table View
    [self setupTableView];
}
1
2
3
4
— (void)setupTableView {
    // Register Class for Cell Reuse
    [self.tableView registerClass:[MTEpisodeCell class] forCellReuseIdentifier:EpisodeCell];
}

Прежде чем мы построим проект и запустим приложение, нам нужно обновить нашу реализацию tableView:cellForRowAtIndexPath: как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MTEpisodeCell *cell = (MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier:EpisodeCell forIndexPath:indexPath];
 
    // Fetch Feed Item
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
 
    // Configure Table View Cell
    [cell.textLabel setText:feedItem.title];
    [cell.detailTextLabel setText:[NSString stringWithFormat:@»%@», feedItem.date]];
 
    return cell;
}

Запустите ваше приложение в iOS Simulator или на тестовом устройстве, чтобы увидеть результат. Если ничего не изменилось, значит, вы правильно выполнили все действия. Все, что мы сделали до сих пор, это заменили прототипные ячейки экземплярами MTEpisodeCell .


Чтобы разрешить загрузку вне процесса, нам нужен сеанс, настроенный для поддержки загрузок вне процесса. Это на удивление легко сделать с помощью NSURLSession API. Там есть несколько ошибок, хотя.

Начните с объявления нового session свойства типа NSURLSession в классе NSURLSessionDelegate и NSURLSessionDownloadDelegate протоколами NSURLSessionDelegate и NSURLSessionDownloadDelegate .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
#import «MTViewController.h»
 
#import «MWFeedParser.h»
#import «SVProgressHUD.h»
#import «MTEpisodeCell.h»
 
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate, MWFeedParserDelegate>
 
@property (strong, nonatomic) NSDictionary *podcast;
@property (strong, nonatomic) NSMutableArray *episodes;
@property (strong, nonatomic) MWFeedParser *feedParser;
 
@property (strong, nonatomic) NSURLSession *session;
 
@end

В viewDidLoad мы устанавливаем свойство session , вызывая backgroundSession для экземпляра контроллера представления. Это одна из ошибок, о которых я говорил.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Setup View
    [self setupView];
 
    // Initialize Session
    [self setSession:[self backgroundSession]];
 
    // Load Podcast
    [self loadPodcast];
 
    // Add Observer
    [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@»MTPodcast» options:NSKeyValueObservingOptionNew context:NULL];
}

Давайте посмотрим на реализацию backgroundSession . В backgroundSession мы статически объявляем переменную session и используем dispatch_once (Grand Central Dispatch) для создания экземпляра фонового сеанса. Хотя это не является строго необходимым, это подчеркивает тот факт, что нам нужен только один фоновый сеанс в любое время. Это лучшая практика, которая также упоминается в сеансе WWDC по API NSURLSession .

В блоке dispatch_once мы начинаем с создания объекта NSURLSessionConfiguration , вызывая backgroundSessionConfiguration: и передавая строку в качестве идентификатора. Идентификатор, который мы передаем, уникально идентифицирует фоновую сессию, которая является ключевой, как мы увидим чуть позже. Затем мы создаем экземпляр сеанса, вызывая sessionWithConfiguration:delegate:delegateQueue: и передавая объект конфигурации сеанса, устанавливая свойство delegate сеанса и передавая nil в качестве третьего аргумента.

01
02
03
04
05
06
07
08
09
10
11
12
13
— (NSURLSession *)backgroundSession {
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Session Configuration
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:@»com.mobiletuts.Singlecast.BackgroundSession»];
 
        // Initialize Session
        session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
    });
 
    return session;
}
sessionWithConfiguration:delegate:delegateQueue: nil в качестве третьего аргумента sessionWithConfiguration:delegate:delegateQueue: сеанс создает для нас очередь последовательной операции. Эта очередь операций используется для выполнения вызовов метода делегата и вызовов обработчика завершения.

Пришло время использовать фоновый сеанс, который мы создали, и использовать MTEpisodeCell . Давайте начнем с реализации tableView:didSelectRowAtIndexPath: метода протокола UITableViewDelegate . Его реализация проста, как вы можете видеть ниже. Мы MWFeedItem правильный экземпляр MWFeedItem из массива episodes и передаем его в downloadEpisodeWithFeedItem:

1
2
3
4
5
6
7
8
9
— (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
 
    // Fetch Feed Item
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
 
    // Download Episode with Feed Item
    [self downloadEpisodeWithFeedItem:feedItem];
}

В downloadEpisodeWithFeedItem: мы извлекаем удаленный URL из элемента фида, вызывая urlForFeedItem: создаем задачу загрузки, вызывая downloadTaskWithURL: в фоновом сеансе, и отправляем ему сообщение resume чтобы запустить задачу загрузки.

1
2
3
4
5
6
7
8
9
— (void)downloadEpisodeWithFeedItem:(MWFeedItem *)feedItem {
    // Extract URL for Feed Item
    NSURL *URL = [self urlForFeedItem:feedItem];
 
    if (URL) {
        // Schedule Download Task
        [[self.session downloadTaskWithURL:URL] resume];
    }
}

Как вы уже догадались, urlForFeedItem: это удобный метод, который мы используем. Мы будем использовать его еще несколько раз в этом проекте. Мы получаем ссылку на массив enclosures элемента канала, извлекаем первый корпус и вытаскиваем объект для ключа url . Мы создаем и возвращаем экземпляр NSURL .

01
02
03
04
05
06
07
08
09
10
11
12
13
— (NSURL *)urlForFeedItem:(MWFeedItem *)feedItem {
    NSURL *result = nil;
 
    // Extract Enclosures
    NSArray *enclosures = [feedItem enclosures];
    if (!enclosures || !enclosures.count) return result;
 
    NSDictionary *enclosure = [enclosures objectAtIndex:0];
    NSString *urlString = [enclosure objectForKey:@»url»];
    result = [NSURL URLWithString:urlString];
 
    return result;
}

Мы еще не закончили. Компилятор дает вам три предупреждения? Это неудивительно, поскольку мы еще не реализовали обязательные методы протоколов NSURLSessionDelegate и NSURLSessionDownloadDelegate . Нам также необходимо реализовать эти методы, если мы хотим показать ход выполнения задач загрузки.

Первый метод, который нам нужно реализовать, это URLSession:downloadTask:didResumeAtOffset: Этот метод вызывается, если задача загрузки возобновлена. Поскольку это то, что мы не рассмотрим в этом руководстве, мы просто записываем сообщение в консоль Xcode.

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

Более интересной является реализация URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: Этот метод вызывается каждый раз, когда сеанс загружает несколько байтов. В этом методе делегата мы вычисляем прогресс, выбираем правильную ячейку и обновляем свойство прогресса ячейки, которое, в свою очередь, обновляет представление прогресса ячейки. Вы заметили вызов dispatch_async ? Нет гарантии, что метод делегата вызывается в основном потоке. Поскольку мы обновляем пользовательский интерфейс, устанавливая прогресс ячейки, нам нужно обновить свойство progress ячейки в главном потоке.

01
02
03
04
05
06
07
08
09
10
11
— (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    // Calculate Progress
    double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
 
    // Update Table View Cell
    MTEpisodeCell *cell = [self cellForForDownloadTask:downloadTask];
 
    dispatch_async(dispatch_get_main_queue(), ^{
        [cell setProgress:progress];
    });
}

Реализация cellForForDownloadTask: проста. Мы извлекаем удаленный URL из задачи загрузки, используя его свойство originalRequest и перебираем элементы фида в массиве episodes пока не найдем совпадение. Когда мы нашли совпадение, мы запрашиваем в табличном представлении соответствующую ячейку и возвращаем ее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
— (MTEpisodeCell *)cellForForDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
    // Helpers
    MTEpisodeCell *cell = nil;
    NSURL *URL = [[downloadTask originalRequest] URL];
 
    for (MWFeedItem *feedItem in self.episodes) {
        NSURL *feedItemURL = [self urlForFeedItem:feedItem];
 
        if ([URL isEqual:feedItemURL]) {
            NSUInteger index = [self.episodes indexOfObject:feedItem];
            cell = (MTEpisodeCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
            break;
        }
    }
 
    return cell;
}

Третий метод NSURLSessionDownloadDelegate протокола NSURLSessionDownloadDelegate , который нам нужно реализовать, — это URLSession:downloadTask:didFinishDownloadingToURL: Как я упоминал в предыдущих уроках, одно из преимуществ API NSURLSession заключается в том, что загружаемые файлы немедленно записываются на диск. В результате мы передаем локальный URL в URLSession:downloadTask:didFinishDownloadingToURL: Однако локальный URL, который мы получаем, указывает на временный файл. Мы несем ответственность за перемещение файла в более постоянное место, и это именно то, что мы делаем в URLSession:downloadTask:didFinishDownloadingToURL:

1
2
3
4
— (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    // Write File to Disk
    [self moveFileWithURL:location downloadTask:downloadTask];
}

В moveFileWithURL:downloadTask: мы извлекаем имя файла эпизода из задачи загрузки и создаем URL в каталоге документов приложения, вызывая URLForEpisodeWithName: Если временный файл, который мы получили из фонового сеанса, указывает на допустимый файл, мы перемещаем этот файл в его новый домашний каталог в каталоге документов приложения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (void)moveFileWithURL:(NSURL *)URL downloadTask:(NSURLSessionDownloadTask *)downloadTask {
    // Filename
    NSString *fileName = [[[downloadTask originalRequest] URL] lastPathComponent];
 
    // Local URL
    NSURL *localURL = [self URLForEpisodeWithName:fileName];
 
    NSFileManager *fm = [NSFileManager defaultManager];
 
    if ([fm fileExistsAtPath:[URL path]]) {
        NSError *error = nil;
        [fm moveItemAtURL:URL toURL:localURL error:&error];
 
        if (error) {
            NSLog(@»Unable to move temporary file to destination. %@, %@», error, error.userInfo);
        }
    }
}
Я использую много вспомогательных методов в своих проектах iOS, потому что это делает для СУХОГО кода. Также хорошей практикой является создание методов, которые делают только одну вещь. Таким образом, тестирование становится намного проще.

URLForEpisodeWithName: еще один вспомогательный метод, который вызывает episodesDirectory . В URLForEpisodeWithName: мы добавляем аргумент name в каталог Episodes , который находится в каталоге документов приложения.

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

В episodesDirectory мы создаем URL для каталога Episodes и создаем каталог, если он еще не существует.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
— (NSURL *)episodesDirectory {
    NSURL *documents = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *episodes = [documents URLByAppendingPathComponent:@»Episodes»];
 
    NSFileManager *fm = [NSFileManager defaultManager];
 
    if (![fm fileExistsAtPath:[episodes path]]) {
        NSError *error = nil;
        [fm createDirectoryAtURL:episodes withIntermediateDirectories:YES attributes:nil error:&error];
 
        if (error) {
            NSLog(@»Unable to create episodes directory. %@, %@», error, error.userInfo);
        }
    }
 
    return episodes;
}

Запустите приложение и проверьте результат, загрузив эпизод из списка эпизодов. Вы должны увидеть прогресс выполнения в ячейке табличного представления слева направо, отражающий ход выполнения задачи загрузки. Есть несколько вопросов, хотя. Вы пробовали прокрутить представление таблицы? Это не выглядит правильно. Давайте это исправим.


Поскольку табличное представление максимально использует ячейки, мы должны убедиться, что каждая ячейка правильно отражает состояние загрузки эпизода, который она представляет. Мы можем исправить это несколькими способами. Один из подходов заключается в использовании объекта, который отслеживает ход выполнения каждой задачи загрузки, включая задачи загрузки, которые уже выполнены.

Начнем с объявления нового частного свойства progressBuffer типа NSMutableDictionary в классе MTViewController .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#import «MTViewController.h»
 
#import «MWFeedParser.h»
#import «SVProgressHUD.h»
#import «MTEpisodeCell.h»
 
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate, MWFeedParserDelegate>
 
@property (strong, nonatomic) NSDictionary *podcast;
@property (strong, nonatomic) NSMutableArray *episodes;
@property (strong, nonatomic) MWFeedParser *feedParser;
 
@property (strong, nonatomic) NSURLSession *session;
@property (strong, nonatomic) NSMutableDictionary *progressBuffer;
 
@end

В viewDidLoad мы инициализируем буфер прогресса, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Setup View
    [self setupView];
 
    // Initialize Session
    [self setSession:[self backgroundSession]];
 
    // Initialize Progress Buffer
    [self setProgressBuffer:[NSMutableDictionary dictionary]];
 
    // Load Podcast
    [self loadPodcast];
 
    // Add Observer
    [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@»MTPodcast» options:NSKeyValueObservingOptionNew context:NULL];
}

Ключ, который мы будем использовать в словаре, — это удаленный URL-адрес соответствующего элемента канала. Имея это в виду, мы можем обновить tableView:cellForRowAtIndexPath: метод, как показано ниже. Мы извлекаем удаленный URL из элемента фида и запрашиваем значение progressBuffer для ключа, который соответствует удаленному URL. Если значение не равно nil , мы устанавливаем свойство progress ячейки в это значение, в противном случае мы устанавливаем свойство progress ячейки в 0.0 , что скрывает представление прогресса, устанавливая ее ширину в 0.0 .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MTEpisodeCell *cell = (MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier:EpisodeCell forIndexPath:indexPath];
 
    // Fetch Feed Item
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
    NSURL *URL = [self urlForFeedItem:feedItem];
 
    // Configure Table View Cell
    [cell.textLabel setText:feedItem.title];
    [cell.detailTextLabel setText:[NSString stringWithFormat:@»%@», feedItem.date]];
 
    NSNumber *progress = [self.progressBuffer objectForKey:[URL absoluteString]];
    if (!progress) progress = @(0.0);
 
    [cell setProgress:[progress floatValue]];
 
    return cell;
}

Мы также можем использовать буфер прогресса, чтобы запретить пользователям загружать один и тот же эпизод дважды. Взгляните на обновленную реализацию tableView:didSelectRowAtIndexPath: Мы предпринимаем те же шаги, что и в tableView:cellForRowAtIndexPath: для извлечения значения прогресса из буфера хода выполнения. Только когда значение прогресса равно nil , мы загружаем эпизод.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
— (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
 
    // Fetch Feed Item
    MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row];
 
    // URL for Feed Item
    NSURL *URL = [self urlForFeedItem:feedItem];
 
    if (![self.progressBuffer objectForKey:[URL absoluteString]]) {
        // Download Episode with Feed Item
        [self downloadEpisodeWithFeedItem:feedItem];
    }
}

Буфер прогресса работает только в своей текущей реализации, если мы будем поддерживать его в актуальном состоянии. Это означает, что нам также необходимо обновить URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: метод. Все, что мы делаем, это сохраняем новое значение прогресса в буфере прогресса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    // Calculate Progress
    double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
 
    // Update Progress Buffer
    NSURL *URL = [[downloadTask originalRequest] URL];
    [self.progressBuffer setObject:@(progress) forKey:[URL absoluteString]];
 
    // Update Table View Cell
    MTEpisodeCell *cell = [self cellForForDownloadTask:downloadTask];
 
    dispatch_async(dispatch_get_main_queue(), ^{
        [cell setProgress:progress];
    });
}

В downloadEpisodeWithFeedItem: мы устанавливаем значение прогресса в 0.0 когда запускается задача загрузки.

01
02
03
04
05
06
07
08
09
10
11
12
— (void)downloadEpisodeWithFeedItem:(MWFeedItem *)feedItem {
    // Extract URL for Feed Item
    NSURL *URL = [self urlForFeedItem:feedItem];
 
    if (URL) {
        // Schedule Download Task
        [[self.session downloadTaskWithURL:URL] resume];
 
        // Update Progress Buffer
        [self.progressBuffer setObject:@(0.0) forKey:[URL absoluteString]];
    }
}

Делегат сеанса получает уведомление о завершении задачи загрузки. В URLSession:downloadTask:didFinishDownloadingToURL: мы устанавливаем значение прогресса в 1.0 .

1
2
3
4
5
6
7
8
— (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    // Write File to Disk
    [self moveFileWithURL:location downloadTask:downloadTask];
 
    // Update Progress Buffer
    NSURL *URL = [[downloadTask originalRequest] URL];
    [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]];
}

На данный момент буфер выполнения хранится только в памяти, что означает, что он очищается между запусками приложения. Мы можем записать его содержимое на диск, но для простоты этого приложения мы собираемся восстановить или воссоздать буфер, проверив, какие эпизоды уже были загружены. feedParser:didParseFeedItem: метод, MWFeedParserDelegate протокола MWFeedParserDelegate , вызывается для каждого элемента в ленте. В этом методе мы извлекаем удаленный URL из элемента канала, создаем соответствующий локальный URL и проверяем, существует ли файл. Если это так, то мы устанавливаем соответствующее значение прогресса для этого элемента канала на 1.0 чтобы указать, что он уже был загружен.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (void)feedParser:(MWFeedParser *)parser didParseFeedItem:(MWFeedItem *)item {
    if (!self.episodes) {
        self.episodes = [NSMutableArray array];
    }
 
    [self.episodes addObject:item];
 
    // Update Progress Buffer
    NSURL *URL = [self urlForFeedItem:item];
    NSURL *localURL = [self URLForEpisodeWithName:[URL lastPathComponent]];
 
    if ([[NSFileManager defaultManager] fileExistsAtPath:[localURL path]]) {
        [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]];
    }
}

Запустите приложение еще раз, чтобы увидеть, решены ли проблемы с табличным представлением. Приложение теперь должно также помнить, какие эпизоды уже были загружены.


Важно, чтобы наше приложение было хорошим гражданином, не тратя больше процессорных циклов и не потребляя больше энергии аккумулятора, чем необходимо. Что это значит для нашего клиента подкаста. Когда наше приложение запускает задачу загрузки и приложение переходит в фоновый режим, фоновый демон, который управляет задачей загрузки нашего приложения, уведомляет наше приложение в фоновом сеансе о завершении задачи загрузки. При необходимости фоновый демон запустит наше приложение, чтобы он мог отвечать на эти уведомления и обрабатывать загруженный файл.

В нашем примере нам не нужно делать ничего особенного, чтобы убедиться, что наше приложение повторно подключается к исходному фоновому сеансу. Об этом позаботится экземпляр MTViewController . Тем не менее, мы должны уведомить операционную систему, когда наше приложение завершит обработку загрузок, вызвав фоновый обработчик завершения.

Когда наше приложение разбудило операционную систему, чтобы ответить на уведомления о фоновом сеансе, делегату приложения отправляется сообщение application:handleEventsForBackgroundURLSession:completionHandler: В этом методе мы можем при необходимости повторно подключиться к фоновому сеансу и вызвать обработчик завершения, который передается нам. Вызывая обработчик завершения, операционная система знает, что нашему приложению больше не нужно работать в фоновом режиме. Это важно для оптимизации срока службы батареи. Как мы делаем это на практике?

Сначала нам нужно объявить свойство в классе MTAppDelegate чтобы сохранить ссылку на обработчик завершения, который мы получаем из application:handleEventsForBackgroundURLSession:completionHandler: Собственность должна быть публичной. Причина этого станет ясна через мгновение.

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

В application:handleEventsForBackgroundURLSession:completionHandler: мы храним обработчик завершения в backgroundSessionCompletionHandler , который мы объявили некоторое время назад.

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

В классе MTViewController мы начинаем с добавления оператора импорта для класса MTAppDelegate .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
#import «MTViewController.h»
 
#import «MWFeedParser.h»
#import «MTAppDelegate.h»
#import «SVProgressHUD.h»
#import «MTEpisodeCell.h»
 
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate, MWFeedParserDelegate>
 
@property (strong, nonatomic) NSDictionary *podcast;
@property (strong, nonatomic) NSMutableArray *episodes;
@property (strong, nonatomic) MWFeedParser *feedParser;
 
@property (strong, nonatomic) NSURLSession *session;
@property (strong, nonatomic) NSMutableDictionary *progressBuffer;
 
@end

Затем мы реализуем другой вспомогательный метод invokeBackgroundSessionCompletionHandler , который вызывает обработчик завершения фона, сохраненный в свойстве backgroundSessionCompletionHandler делегата приложения. В этом методе мы запрашиваем фоновый сеанс для всех выполняющихся задач. Если задачи не выполняются, мы получаем ссылку на фоновый обработчик завершения делегата приложения и, если он не равен nil , мы вызываем его и устанавливаем значение nil .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (void)invokeBackgroundSessionCompletionHandler {
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        NSUInteger count = [dataTasks count] + [uploadTasks count] + [downloadTasks count];
 
        if (!count) {
            MTAppDelegate *applicationDelegate = (MTAppDelegate *)[[UIApplication sharedApplication] delegate];
            void (^backgroundSessionCompletionHandler)() = [applicationDelegate backgroundSessionCompletionHandler];
 
            if (backgroundSessionCompletionHandler) {
                [applicationDelegate setBackgroundSessionCompletionHandler:nil];
                backgroundSessionCompletionHandler();
            }
        }
    }];
}

Подожди минуту. Когда мы вызываем invokeBackgroundSessionCompletionHandler ? Мы делаем это каждый раз, когда заканчивается задача загрузки. Другими словами, мы вызываем этот метод в URLSession:downloadTask:didFinishDownloadingToURL: как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
— (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    // Write File to Disk
    [self moveFileWithURL:location downloadTask:downloadTask];
 
    // Update Progress Buffer
    NSURL *URL = [[downloadTask originalRequest] URL];
    [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]];
 
    // Invoke Background Completion Handler
    [self invokeBackgroundSessionCompletionHandler];
}

Надеюсь, вы согласны с тем, что наш клиент подкастов еще не готов к App Store, поскольку одна из ключевых функций — воспроизведение подкастов — по-прежнему отсутствует. Как я упоминал в предыдущем уроке, в центре внимания этого проекта не было создание полнофункционального клиента подкастов. Целью этого проекта было показать, как использовать API NSURLSession для поиска в API поиска iTunes и загрузки эпизодов подкастов с использованием данных и задач загрузки вне процесса, соответственно. Теперь у вас должно быть базовое понимание API NSURLSession а также задач вне процесса.


Создав простой клиент подкаста, мы внимательно изучили данные и загрузку. Мы также узнали, как легко планировать задачи загрузки в фоновом режиме. API NSURLSession — важный шаг вперед для iOS и OS X, и я призываю вас воспользоваться этим простым в использовании и гибким набором классов. В последней части этой серии статей я рассмотрю AFNetworking 2.0. Почему это вехой релиз? Когда вы должны его использовать? И как это по сравнению с NSURLSession API?