В предыдущем уроке мы начали создавать простой клиент подкаста, чтобы NSURLSession
на практике то, что мы узнали о NSURLSession
. Пока что наш клиент подкастов может запрашивать API-интерфейс поиска iTunes, загружать канал подкастов и отображать список эпизодов. В этом уроке мы рассмотрим еще один интересный аспект NSURLSession
вне процесса. Позвольте мне показать вам, как это работает.
Вступление
В этом четвертом и последнем уроке о NSURLSession
мы более подробно рассмотрим задачи, не связанные с процессом, в частности задачи загрузки. Наш клиент подкаста уже может показать список эпизодов, но в настоящее время ему не хватает возможности загружать отдельные эпизоды. Это будет основной темой этого урока.
Фоновые загрузки и загрузки
Добавление поддержки фоновых загрузок и загрузок на удивление легко с NSURLSession
. Apple называет их загрузками вне процесса, так как задачи управляются фоновым демоном, а не вашим приложением. Даже если ваше приложение вылетает во время задачи загрузки или выгрузки, эта задача остается в фоновом режиме.
обзор
Я хотел бы уделить несколько минут, чтобы поближе взглянуть на то, как работают внепроцессные задачи. Это довольно просто, когда у вас есть полная картина процесса. Включение фоновых загрузок и загрузок — это не что иное, как переключение переключателя в конфигурации вашего сеанса. С правильно настроенным объектом сеанса вы готовы планировать загрузку и загрузку задач в фоновом режиме.
Когда начинается загрузка или загрузка, появляется фоновый демон. Демон выполняет задачу и отправляет обновления в приложение через протоколы делегатов, объявленные в NSURLSession
API. Если ваше приложение по какой-то причине перестает работать, задача продолжает работать в фоновом режиме, поскольку это демон, управляющий задачей. В момент завершения задачи приложение, создавшее задачу, получает уведомление. Он повторно подключается к фоновому сеансу, который создал задачу, и демон, управляющий задачей, информирует сеанс о завершении задачи и, в случае задачи загрузки, передает файл сеансу. Затем сеанс вызывает соответствующие методы делегата, чтобы убедиться, что ваше приложение может предпринять соответствующие действия, такие как перемещение файла в более постоянное место. Этого достаточно для теории. Давайте посмотрим, что нам нужно сделать для реализации загрузок вне процесса в Singlecast.
1. Подкласс UITableViewCell
Шаг 1: Обновление главной раскадровки
На данный момент мы используем прототип ячейки для заполнения табличного представления. Чтобы дать нам немного большей гибкости, нам нужно создать подкласс UITableViewCell
. Откройте основную раскадровку, выберите табличное представление экземпляра MTViewController
и установите количество ячеек прототипа MTViewController
0
.
Шаг 2: Создать подкласс
Откройте меню Файл XCode и выберите New> File …. Создайте новый класс Objective-C, назовите его MTEpisodeCell
и убедитесь, что он наследуется от UITableViewCell
. Скажите Xcode, где вы хотите хранить файлы классов, и нажмите « Создать» .
Шаг 3: Обновить интерфейс класса
Интерфейс MTEpisodeCell
прост, как вы можете видеть из фрагмента кода ниже. Все что мы делаем — объявляем свойство progress
типа float
. Мы будем использовать это для обновления и отображения хода выполнения задачи загрузки, которую мы будем использовать для загрузки эпизода.
1
2
3
4
5
6
7
|
#import <UIKit/UIKit.h>
@interface MTEpisodeCell : UITableViewCell
@property (assign, nonatomic) float progress;
@end
|
Шаг 4: реализовать класс
Реализация 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;
}
|
Шаг 5: Используйте MTEpisodeCell
Чтобы использовать 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;
}
|
Шаг 6: Сборка и запуск
Запустите ваше приложение в iOS Simulator или на тестовом устройстве, чтобы увидеть результат. Если ничего не изменилось, значит, вы правильно выполнили все действия. Все, что мы сделали до сих пор, это заменили прототипные ячейки экземплярами MTEpisodeCell
.
2. Создайте фоновую сессию
Чтобы разрешить загрузку вне процесса, нам нужен сеанс, настроенный для поддержки загрузок вне процесса. Это на удивление легко сделать с помощью NSURLSession
API. Там есть несколько ошибок, хотя.
Шаг 1: Создать свойство session
Начните с объявления нового 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:
сеанс создает для нас очередь последовательной операции. Эта очередь операций используется для выполнения вызовов метода делегата и вызовов обработчика завершения. 3. Скачать Эпизод
Шаг 1: Создать задачу загрузки
Пришло время использовать фоновый сеанс, который мы создали, и использовать 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
. Нам также необходимо реализовать эти методы, если мы хотим показать ход выполнения задач загрузки.
Шаг 2: Реализация протокола (ов)
Первый метод, который нам нужно реализовать, это 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);
}
}
}
|
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;
}
|
Шаг 3: Сборка и запуск
Запустите приложение и проверьте результат, загрузив эпизод из списка эпизодов. Вы должны увидеть прогресс выполнения в ячейке табличного представления слева направо, отражающий ход выполнения задачи загрузки. Есть несколько вопросов, хотя. Вы пробовали прокрутить представление таблицы? Это не выглядит правильно. Давайте это исправим.
4. Создайте буфер прогресса
Поскольку табличное представление максимально использует ячейки, мы должны убедиться, что каждая ячейка правильно отражает состояние загрузки эпизода, который она представляет. Мы можем исправить это несколькими способами. Один из подходов заключается в использовании объекта, который отслеживает ход выполнения каждой задачи загрузки, включая задачи загрузки, которые уже выполнены.
Шаг 1: объявить собственность
Начнем с объявления нового частного свойства 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
|
Шаг 2: инициализировать буфер
В 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];
}
|
Шаг 3: Обновление ячеек табличного представления
Ключ, который мы будем использовать в словаре, — это удаленный 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;
}
|
Шаг 4: избегайте дубликатов
Мы также можем использовать буфер прогресса, чтобы запретить пользователям загружать один и тот же эпизод дважды. Взгляните на обновленную реализацию 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];
}
}
|
Шаг 5: Обновить буфер
Буфер прогресса работает только в своей текущей реализации, если мы будем поддерживать его в актуальном состоянии. Это означает, что нам также необходимо обновить 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]];
}
|
Шаг 6: Восстановить буфер
На данный момент буфер выполнения хранится только в памяти, что означает, что он очищается между запусками приложения. Мы можем записать его содержимое на диск, но для простоты этого приложения мы собираемся восстановить или воссоздать буфер, проверив, какие эпизоды уже были загружены. 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]];
}
}
|
Шаг 7: промойте и повторите
Запустите приложение еще раз, чтобы увидеть, решены ли проблемы с табличным представлением. Приложение теперь должно также помнить, какие эпизоды уже были загружены.
5. Быть хорошим гражданином
Важно, чтобы наше приложение было хорошим гражданином, не тратя больше процессорных циклов и не потребляя больше энергии аккумулятора, чем необходимо. Что это значит для нашего клиента подкаста. Когда наше приложение запускает задачу загрузки и приложение переходит в фоновый режим, фоновый демон, который управляет задачей загрузки нашего приложения, уведомляет наше приложение в фоновом сеансе о завершении задачи загрузки. При необходимости фоновый демон запустит наше приложение, чтобы он мог отвечать на эти уведомления и обрабатывать загруженный файл.
В нашем примере нам не нужно делать ничего особенного, чтобы убедиться, что наше приложение повторно подключается к исходному фоновому сеансу. Об этом позаботится экземпляр MTViewController
. Тем не менее, мы должны уведомить операционную систему, когда наше приложение завершит обработку загрузок, вызвав фоновый обработчик завершения.
Когда наше приложение разбудило операционную систему, чтобы ответить на уведомления о фоновом сеансе, делегату приложения отправляется сообщение application:handleEventsForBackgroundURLSession:completionHandler:
В этом методе мы можем при необходимости повторно подключиться к фоновому сеансу и вызвать обработчик завершения, который передается нам. Вызывая обработчик завершения, операционная система знает, что нашему приложению больше не нужно работать в фоновом режиме. Это важно для оптимизации срока службы батареи. Как мы делаем это на практике?
Шаг 1: объявить собственность
Сначала нам нужно объявить свойство в классе 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
|
Шаг 2: Реализация обратного вызова
В application:handleEventsForBackgroundURLSession:completionHandler:
мы храним обработчик завершения в backgroundSessionCompletionHandler
, который мы объявили некоторое время назад.
1
2
3
|
— (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
[self setBackgroundSessionCompletionHandler:completionHandler];
}
|
Шаг 3: вызов фонового обработчика завершения
В классе 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];
}
|
6. Завершение
Надеюсь, вы согласны с тем, что наш клиент подкастов еще не готов к App Store, поскольку одна из ключевых функций — воспроизведение подкастов — по-прежнему отсутствует. Как я упоминал в предыдущем уроке, в центре внимания этого проекта не было создание полнофункционального клиента подкастов. Целью этого проекта было показать, как использовать API NSURLSession
для поиска в API поиска iTunes и загрузки эпизодов подкастов с использованием данных и задач загрузки вне процесса, соответственно. Теперь у вас должно быть базовое понимание API NSURLSession
а также задач вне процесса.
Вывод
Создав простой клиент подкаста, мы внимательно изучили данные и загрузку. Мы также узнали, как легко планировать задачи загрузки в фоновом режиме. API NSURLSession
— важный шаг вперед для iOS и OS X, и я призываю вас воспользоваться этим простым в использовании и гибким набором классов. В последней части этой серии статей я рассмотрю AFNetworking 2.0. Почему это вехой релиз? Когда вы должны его использовать? И как это по сравнению с NSURLSession
API?