Музыкальное приложение с помощью NSURLSession

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

Извлекает ли приложение данные с сервера, обновляет ли ваш статус в соц. сетях или загружает удаленно находящиеся файлы на диск - это работа сетевых запросов HTTP, живущих в центре мобильных приложений, и именно они "ответственны" за волшебство. Чтобы помочь вам с многочисленными требованиями к сетевым запросам, Apple предоставляет NSURLSession, которая является полным набором сетевых методов API для загрузки и скачивания контента через HTTP.

В этом туториале по NSURLSession вы узнаете, как использовать NSURLSession в приложении Half Tunes, позволяющем запрашивать iTunes Search API и загружать 30-секундные превью выбираемых песен. Законченное приложение также будет поддерживать загрузку в фоновом режиме и давать возможность пользователю ставить паузу, возобновлять или отменять загрузку незавершенных процессов.

Поехали!

Загрузите стартовый проект, так как он уже содержит пользовательский интерфейс для поиска песен и отображения результатов поиска, а также некоторые вспомогательные методы для парсинга формата JSON и воспроизведения треков. Это позволит вам сосредоточиться только на реализации сетевых аспектов приложения.

Запустите проект. Вы должны увидеть view в верхней части search bar и пустой table view ниже:

Введите запрос в поисковой строке и нажмите Search (Поиск). View останется пустым, но не волнуйтесь, вы сможете это изменить через вызовы NSURLSession.

Обзор NSURLSession

До того как мы приступим, давайте разберем NSURLSession и его составные классы. Краткий обзор займет минуту.

Технически NSURLSession это и класс и набор классов для обработки запросов, основанных на HTTP.

NSURLSession является ключевым объектом, отвечающим за отправку и прием HTTP запросов. Вы можете создать его с помощью NSURLSessionConfiguration, который идет в трех вариантах:

  • defaultSessionConfiguration: Создает объект дефолтных настроек по умолчанию, использующих сохраняемый на диске глобальный кэш, учетные записи и хранилище куки.
  • ephemeralSessionConfiguration: этот вариант подобен настройкам по умолчанию, за исключением того, что все данные, связанные с сеансом, сохраняются в памяти. Это можно представить как "частную” сессию.
  • backgroundSessionConfiguration: Позволяет сессии выполнить задание по загрузке данных на сервер или с сервера в фоновом режиме. Передача данных продолжается даже тогда, когда приложение приостановлено или его работа прекращена.
  • NSURLSessionConfiguration также позволяет настроить вам свойства сеанса, такие как: величину таймаута, политику кэширования и дополнительных заголовков HTTP. Обратитесь к документации для полного списка доступных опций.
  • NSURLSessionTask является абстрактным классом, который обозначает объект задания. Сеанс создает задание, который выполняет всю работу поиска данных и загрузки на сервер или с сервера файлов.

Есть три типа конкретных заданий сессии в этом контексте:

  • NSURLSessionDataTask: Используйте это задание для HTTP GET запросов для извлечения в память данных с серверов.
  • NSURLSessionUploadTask: Используйте это задание, чтобы загрузить файл с диска на веб-сервис, как правило, через POST HTTP или метод PUT.
  • NSURLSessionDownloadTask: Используйте это задание, чтобы загрузить файл с удаленного сервиса во временную папку файла.

Вы также можете приостанавливать, возобновлять и отменять задания. NSURLSessionDownloadTask имеет дополнительную возможность приостановки с последующим возобновлением работы.

Вообще, NSURLSession возвращает данные двумя способами: с помощью завершающего обработчика, когда задание заканчивается либо успешно или с ошибкой, или путем вызова методов делегата, который вы установили при создании сеанса.

Теперь, когда вы представляете, что можно сделать с помощью NSURLSession, давайте применим теорию на практике!

Запросы треков

Начнем с добавления кода для запроса iTunes Search API, когда пользователь ищет треки.

В SearchViewController.swift добавьте следующий код в верхнюю часть класса:

// 1
let defaultSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
// 2
var dataTask: NSURLSessionDataTask?

Вот, что мы сделали:

  1. Вы создаете NSURLSession и инициализируете его с дефолтными настройками по умолчанию.
  2. Вы объявляете переменную NSURLSessionDataTask, которую вы будете использовать для запроса HTTP GET на iTunes Search, когда пользователь выполняет поиск. Задание на получение данных будет повторно инициализированно и повторно использовано каждый раз, когда пользователь создает новый запрос.

Теперь, замените searchBarSearchButtonClicked(_:) следующим:

func searchBarSearchButtonClicked(searchBar: UISearchBar) {
  dismissKeyboard()
 
  if !searchBar.text!.isEmpty {
    // 1
    if dataTask != nil {
      dataTask?.cancel()
    }
    // 2
    UIApplication.sharedApplication().networkActivityIndicatorVisible = true
    // 3
    let expectedCharSet = NSCharacterSet.URLQueryAllowedCharacterSet()
    let searchTerm = searchBar.text!.stringByAddingPercentEncodingWithAllowedCharacters(expectedCharSet)!
    // 4
    let url = NSURL(string: "https://itunes.apple.com/search?media=music&entity=song&term=\(searchTerm)")
    // 5
    dataTask = defaultSession.dataTaskWithURL(url!) {
      data, response, error in
      // 6
      dispatch_async(dispatch_get_main_queue()) {
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
      }
      // 7
      if let error = error {
        print(error.localizedDescription)
      } else if let httpResponse = response as? NSHTTPURLResponse {
        if httpResponse.statusCode == 200 {
          self.updateSearchResults(data)
        }
      }
    }
    // 8
    dataTask?.resume()
  }
}

Разберем пошагово:

  1. При каждом запросе пользователя, вы проверяете, инициализировано ли уже задание на получение данных. Если да, то вы отменяете задание, так как вы хотите повторно использовать объект задания на получение данных для последнего запроса.
  2. Вы включаете индикатор сетевой активности в status bar, чтобы пользователь мог видеть, что идет процесс работы сети.
  3. Прежде чем передать строку поиска в качестве параметра в URL запрос, вы вызываете stringByAddingPercentEncodingWithAllowedCharacters(_:) на строку для того, чтобы убедиться, что он правильно освобожден (properly escaped.).
  4. Далее вы инициализируете NSURL, добавляя строку поиска в качестве параметра GET iTunes Search API base url.
  5. Из созданной сессии вы инициализируете NSURLSessionDataTask для обработки запроса HTTP GET. Конструктор NSURLSessionDataTask принимает созданный вами NSURL, а также завершающий обработчик, которые мы вызываем, когда задание на передачу данных завершено.
  6. После получения ответа о том, что задача завершена, вы скрываете индикатор активности и вызываете обновление интерфейса в главном потоке.
  7. Если HTTP запрос прошел успешно, вы вызываете updateSearchResults(_:), который парсит ответный NSData в Tracks и обновляет table view.
  8. Все задания начинаются по умолчанию в режиме ожидания, задание на получение данных начнется при вызове resume().

Запустите приложение, вбейте запрос на поиск любой песни, вы должны увидеть, что table view заполнится соответствующими результату треками следующим образом:

Мы добавили немного магии NSURLSession и теперь Half Tunes стал уже более функционален!

Загрузка треков.

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

Для того, чтобы легко обрабатывать несколько загрузок, давайте сначала создадим пользовательский объект, чтобы видеть состояние активного скачивания.

Создайте новый файл с именем Download.swift в группе Data Objects.

Откройте Download.swift и добавьте следующую реализацию:

class Download: NSObject {
 
  var url: String
  var isDownloading = false
  var progress: Float = 0.0
 
  var downloadTask: NSURLSessionDownloadTask?
  var resumeData: NSData?
 
  init(url: String) {
    self.url = url
  }
}

Кратко о свойствах Download:

  • url: URL файла для загрузки. Он также используется в качестве уникального идентификатора для Download.
  • isDownloading: Показывает продолжается ли загрузка, или она приостановлена.
  • progress: Дробный прогресс загрузки, значение которого меняется от 0.0 до 1.0.
  • downloadTask: Это NSURLSessionDownloadTask, который загружает файл.
  • resumeData: сохраняет произведенную NSData, когда вы ставите на паузу загрузку получения данных. Если хост-сервер это поддерживает, то вы можете использовать его для возобновления приостановленной загрузки в будущем.

Переключитесь на SearchViewController.swift и добавьте код в начало вашего класса:

var activeDownloads = [String: Download]()

Это нужно для поддержки отображения между URLs и их Download, если таковые имеются.

Создание задания по загрузке (данных)

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

В SearchViewController.swift добавьте следующий код непосредственно перед viewDidLoad():

lazy var downloadsSession: NSURLSession = {
  let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
  let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
  return session
}()

Здесь вы инициализируете отдельную сессию с дефолтной конфигурацией для обработки всех ваших заданий по загрузке. Также вы можете указать делегата, который позволяет вам получать события (events) NSURLSession с помощью вызовов делегатов. Отслеживание полезно не только тогда, когда задача уже выполнена, но и в процессе выполнения задачи.

Установка очереди делегата на nil приводит к тому, что сессия создает по умолчанию последовательную рабочую очередь, для выполнения всех вызовов методов делегата и завершающих обработчиков.

Обратите внимание на ленивое создание downloadsSession: это позволяет отсрочить создание сессии до того момента, пока вам будет это нужно. Самое главное, что это позволяет передавать self в качестве параметра делегата инициализатору - даже если self не инициализировано.

В SearchViewController.swift найдите пустое расширение NSURLSessionDownloadDelegate и измените его так, чтобы оно выглядело следующим образом:

extension SearchViewController: NSURLSessionDownloadDelegate {
  func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
    print("Finished downloading.")
  }
}

NSURLSessionDownloadDelegate определяет методы делегата, которые нужно реализовать при использовании заданий NSURLSession на загрузку данных. Единственный неопциональный метод - это URLSession(_:downloadTask:didFinishDownloadingToURL:), который вы вызываете, когда заканчивается загрузка. На данный момент, вы печатаете простое сообщение всякий раз, когда завершается загрузка.

С настроенными сеансом и делегатом, вы, наконец, готовы создать задание на загрузку данных, в момент, когда пользователь запрашивает закачку трека.

В SearchViewController.swift замените startDownload(_:) следующей реализацией:

func startDownload(track: Track) {
  if let urlString = track.previewUrl, url =  NSURL(string: urlString) {
    // 1
    let download = Download(url: urlString)
    // 2
    download.downloadTask = downloadsSession.downloadTaskWithURL(url)
    // 3
    download.downloadTask!.resume()
    // 4
    download.isDownloading = true
    // 5
    activeDownloads[download.url] = download
  }
}

Когда вы нажимаете кнопку Download, вы вызываете startDownload(_:) соответствующего треку. Вот что происходит:

  1. Сначала вы инициализируете Download с превью URL трека.
  2. Используя свой новый объект сеанса, вы создаете NSURLSessionDownloadTask с превью URL, и устанавливаете его в свойство downloadTask экземпляра Download.
  3. Вы запускаете задание на загрузку путем вызова resume().
  4. Вы указываете, что загрузка находится в процессе. 
  5. Наконец, вы отмечаете скаченный URL в его Download в словаре activeDownloads.

Запустите приложение, выберете любой трек и нажмите кнопку Download. Вы должны увидеть сообщение, которое появится через некоторое время на консоли, что означает, что загрузка завершена.

Сохраняем и воспроизводим треки

После завершения задания по загрузке, URLSession(_:downloadTask:didFinishDownloadingToURL:) предоставляет URL временное местоположение файлов. Ваша задача переместить его на постоянное место в директорию песочницы контейнера вашего приложения, прежде чем вы вернетесь из метода. Кроме того, вам будет нужно удалить активную загрузку из словаря и обновить table view.

Для простоты давайте добавим вспомогательный метод. В SearchViewController.swift добавьте следующий метод к классу:

func trackIndexForDownloadTask(downloadTask: NSURLSessionDownloadTask) -> Int? {
  if let url = downloadTask.originalRequest?.URL?.absoluteString {
    for (index, track) in searchResults.enumerate() {
      if url == track.previewUrl! {
        return index
      }
    }
  }
  return nil
}

Этот метод просто возвращает индекс Track в списке searchResults, который имеет данный URL.

Затем замените URLSession(_:downloadTask:didFinishDownloadingToURL:) следующим кодом:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
  // 1
  if let originalURL = downloadTask.originalRequest?.URL?.absoluteString,
    destinationURL = localFilePathForUrl(originalURL) {
 
    print(destinationURL)
 
    // 2
    let fileManager = NSFileManager.defaultManager()
    do {
      try fileManager.removeItemAtURL(destinationURL)
    } catch {
      // Non-fatal: file probably doesn't exist
    }
    do {
      try fileManager.copyItemAtURL(location, toURL: destinationURL)
    } catch let error as NSError {
      print("Could not copy file to disk: \(error.localizedDescription)")
    }
  }
 
  // 3
  if let url = downloadTask.originalRequest?.URL?.absoluteString {
    activeDownloads[url] = nil
    // 4
    if let trackIndex = trackIndexForDownloadTask(downloadTask) {
      dispatch_async(dispatch_get_main_queue(), {
        self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: trackIndex, inSection: 0)], withRowAnimation: .None)
      })
    }
  }
}

Разберем пошагово:

  1. Вы извлекаете оригинальный запрос URL из задания и передаете его указанному вспомогательному методу localFilePathForUrl(_:). Затем localFilePathForUrl(_:) генерирует постоянный локальный путь файла для сохранения, путем добавления lastPathComponent в URL (т.е. имя файла и расширение файла) в путь директории документов приложения.
  2. Используя NSFileManager, вы перемещаете загруженный файл из места временного расположения по тому пути, который вам нравится, очистив все пункты в этом местоположении до начала копирования.
  3. Вы находите соответствующий Download в ваших активных загрузках и удаляете его.
  4. Наконец, вы находите Track в вашем table view и перезагружаете соответствующую ячейку.

Запустите ваш проект, выберите любой трек и загрузите его. Когда загрузка будет завершена, вы должны увидеть путь нахождения файла на консоле:

Кнопка Download тоже исчезнет, ​​поскольку трек уже на вашем устройстве. Нажмите на трек, и вы услышите как он начнет проигрываться на представленном MPMoviePlayerViewController, как показано ниже:

Мониторинг выполнения загрузки

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

В SearchViewController.swift найдите расширение, реализующее NSURLSessionDownloadDelegate и добавьте следующий метод делегата:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
 
    // 1
    if let downloadUrl = downloadTask.originalRequest?.URL?.absoluteString,
      download = activeDownloads[downloadUrl] {
      // 2
      download.progress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
      // 3
      let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: NSByteCountFormatterCountStyle.Binary)
      // 4
      if let trackIndex = trackIndexForDownloadTask(downloadTask), let trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: trackIndex, inSection: 0)) as? TrackCell {
        dispatch_async(dispatch_get_main_queue(), {
          trackCell.progressView.progress = download.progress
          trackCell.progressLabel.text =  String(format: "%.1f%% of %@",  download.progress * 100, totalSize)
        })
    }
  }
}

Разберем пошагово:

  1. С помощью прилагаемого downloadTask вы извлекаете URL и используете его, чтобы найти Download в вашем словаре активных загрузок.
  2. Метод также возвращает общее число написанных байтов и общее число ожидаемых к написанию байтов. Вы вычисляете прогресс как отношение двух величин и сохраняете результат в Download. Вы будете использовать это значение, чтобы обновить progress view.
  3. NSByteCountFormatter принимает байтовое значение и генерирует его в читаемую строку, показывающую общий размер загрузки файла. Вы будете использовать эту строку, чтобы показать размер загружаемого файла и процент загрузки.
  4. Наконец, вы находите ячейку, отвечающую за отображение Track, и обновляете и полосу загрузки и соответствующий ярлык значениями, полученными из предыдущих шагов.

Далее, вы будете настраивать ячейки, чтобы они правильно отображали progress view и status (статус), каждый раз, когда будет начинаться загрузка.

Найдите следующую строку кода в tableView(_:cellForRowAtIndexPath:):

let downloaded = localFileExistsForTrack(track)

И добавьте следующий код выше:

var showDownloadControls = false
if let download = activeDownloads[track.previewUrl!] {
  showDownloadControls = true
 
  cell.progressView.progress = download.progress
  cell.progressLabel.text = (download.isDownloading) ? "Downloading..." : "Paused"
}
cell.progressView.hidden = !showDownloadControls
cell.progressLabel.hidden = !showDownloadControls

Для треков с активной загрузкой, вы устанавливаете showDownloadControls на true, в противном случае, на false. Затем отображаете progress views и labels, которые содержит шаблон проекта, в соответствии со значением showDownloadControls.

Для приостановленных загрузок, в статусе появляется "Paused", в другом случае "Downloading…".

Наконец, замените следующую строку:

cell.downloadButton.hidden = downloaded

следующим:

cell.downloadButton.hidden = downloaded || showDownloadControls

Здесь вы говорите ячейке скрыть кнопку Download, если трек начал скачиваться.

Запустите проект, скачайте любой трек и вы должны увидеть обновление статуса progress bar, как будет идти загрузка:

Ура, получилось! :]

Приостановка, возобновление и отмена загрузок

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

Начнем с того, что позволим пользователю отменять активную загрузку.

Заменим cancelDownload(_:) следующим кодом:

func cancelDownload(track: Track) {
  if let urlString = track.previewUrl,
    download = activeDownloads[urlString] {
      download.downloadTask?.cancel()
      activeDownloads[urlString] = nil
  }
}

Чтобы отменить загрузку, вы извлекаете задание на загрузку из соответствующего Download в словаре активных загрузок и вызываете на него cancel(), чтобы отменить задание. Затем удалите его из словаря активных загрузок.

Приостановка загрузки концептуально подобна отмене; разница в том, что пауза отменяет задание на загрузку, и производит resume data, которая содержит достаточно информации, чтобы возобновить загрузку в более позднее время.

Заметка

 

Вы можете возобновить загрузку только при определенных условиях. Например, ресурс не должен был измениться, после того, как вы впервые его запросили. С полным списком условий вы можете ознакомиться здесь.

Теперь, замените pauseDownload(_:) следующим кодом:

func pauseDownload(track: Track) {
  if let urlString = track.previewUrl,
    download = activeDownloads[urlString] {
      if(download.isDownloading) {
        download.downloadTask?.cancelByProducingResumeData { data in
          if data != nil {
            download.resumeData = data
          }
        }
        download.isDownloading = false
      }
  }
}

Основное различие здесь - это вызов cancelByProducingResumeData(_:) вместо cancel(). Вы извлекаете полученные данные из замыкания, предусмотренного cancelByProducingResumeData(:_) и сохраняете его в соответствующий Download для будущего возобновления.

Вы также устанавливаете свойство isDownloading экземпляра Download на false, чтобы показать, что загрузка приостановлена.

С функции паузы мы разобрались, теперь рассмотрим предоставление возобновления приостановленной загрузки.

Заменим resumeDownload(_:) следующим кодом:

func resumeDownload(track: Track) {
  if let urlString = track.previewUrl,
    download = activeDownloads[urlString] {
      if let resumeData = download.resumeData {
        download.downloadTask = downloadsSession.downloadTaskWithResumeData(resumeData)
        download.downloadTask!.resume()
        download.isDownloading = true
      } else if let url = NSURL(string: download.url) {
        download.downloadTask = downloadsSession.downloadTaskWithURL(url)
        download.downloadTask!.resume()
        download.isDownloading = true
      }
  }
}

Когда пользователь возобновляет загрузку, вы проверяете соответствующий Download на наличие полученных данных. Если они найдены, то вы создаете новое задание на загрузку при помощи команды downloadTaskWithResumeData(_:) полученными данными и запускаете задание, вызывая resume(). Если полученные данные по каким-то причинам отсутствуют, то вы создаете новое задание на загрузку с нуля с загрузкой URL в любом случае и запускаете его.

В обоих случаях, вы устанавливаете свойство isDownloading экземпляра Download на true, чтобы указать, что загрузка вновь возобновлена.

Для того, чтобы все работало правильно, осталось для этих трех функций сделать лишь одно: вы должны показывать или скрывать кнопки Pause, Cancel и Resume в зависимости от обстоятельств.

Идем в tableView(_:cellForRowAtIndexPath:) и находим следующую строку кода:

if let download = activeDownloads[track.previewUrl!] {

Добавим следующую строку в конец блока let:

let title = (download.isDownloading) ? "Pause" : "Resume"
cell.pauseButton.setTitle(title, forState: UIControlState.Normal)

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

Теперь добавим следующий код в конце tableView(_:cellForRowAtIndexPath:), перед заявлением return:

cell.pauseButton.hidden = !showDownloadControls
cell.cancelButton.hidden = !showDownloadControls

Здесь вы просто показываете кнопки для ячейки, если только загрузка активна.

Запустите проект, скачайте несколько треков одновременно, и у вас будет возможность приостановить, возобновить и отменить их по своему желанию.

Включение фоновой передачи данных

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

Но если ваше приложение не работает, то как тогда может происходить фоновая передача данных? Существует "демон", который работает вне приложения и управляет заданиями фоновой передачи данных. Он посылает соответствующие сообщения делегатов приложениям в момент, когда происходит загрузка. В случае, если приложение закрывается (заканчивает свою работу) в течение активной передачи, то задания будут продолжать происходить без изменений в фоновом режиме.

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

Заметка

 

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

Так же в SearchViewController.swift в инициализации downloadsSession найдите следующую строку кода:

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()

И замените ее следующим:

let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("bgSessionConfiguration")

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

Далее, в viewDidLoad() добавьте следующий код:

_ = self.downloadsSession

Вызов лениво загруженной downloadsSession гарантирует, что приложение создает ровно один фоновый сеанс при инициализации SearchViewController.

Если фоновое задание завершается в тот момент, когда приложение не запущено, то приложение будет повторно запущено в фоновом режиме. Вам нужно обработать это событие из делегата приложения.

Переключитесь на AppDelegate.swift и добавьте следующий код в верхней части класса:

var backgroundSessionCompletionHandler: (() -> Void)?

Затем добавьте следующий метод AppDelegate.swift:

func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {
  backgroundSessionCompletionHandler = completionHandler
}

Здесь вы сохраняете прилагаемый completionHandler в качестве переменной в делегате вашего приложения для последующего использования.

С помощью application(_:handleEventsForBackgroundURLSession:) приложение начинает работу с уже законченным фоновым заданием. Здесь нужно помнить о двух вещах:

  • Во-первых, приложение должно подключиться к соответствующей фоновой сессии с идентификатором, предоставленным методом делегата. Но так как вы создаете и используете только один фоновый сеанс каждый раз, когда создаете экземпляр SearchViewController, то вы уже к нему переподсоединились!
  • Во-вторых, вы должны захватить завершающий обработчик, представленный методом делегата. Исполнение завершающего обработчика заставляет OS сделать снимок обновленного интерфейса для отображения в переключателе приложений, а также сообщает OS, что все фоновые процессы вашего приложения текущего сеанса закончили свою работу.

Но когда вам следует вызывать завершающий обработчик? Хорошим выбором будет URLSessionDidFinishEventsForBackgroundURLSession(_:). Он является методом NSURLSessionDelegate, который срабатывает когда все задания, относящиеся к фоновой сессии, заканчивают свое выполнение.

Реализуйте следующее расширение в ​​SearchViewController.swift:

extension SearchViewController: NSURLSessionDelegate {
 
  func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) {
    if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate {
      if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
        appDelegate.backgroundSessionCompletionHandler = nil
        dispatch_async(dispatch_get_main_queue(), {
          completionHandler()
        })
      }
    }
  }
}

Приведенный выше код, просто захватывает сохраненный завершающий обработчик из приложения делегата и ссылается на него в основном потоке.

Запустите приложение, запустите несколько параллельных загрузок и нажмите кнопку Home для отправки задач в фоновый режим приложения. Подождите то время, которое должно понадобиться для завершения загрузок, затем дважды нажмите кнопку Home, чтобы открыть переключатель приложений.

Загрузки файлов должны завершиться и их новый статус будет отражен на скриншоте приложения. Откройте приложение, чтобы удостовериться:

Теперь у вас есть функциональное музыкальное приложение! Следующий шаг- Apple Music! :]

Загрузите Конечный проект.

Что дальше?

Дальше, вы можете продолжить изучать наши туториалы по мере их появления, а также, параллельно читать перевод официальной книги по языку программирования Swift. И, для более подробного изучения языка, вы можете пройти наши курсы!

Урок подготовил: Акулов Иван

Источник урока: Источник