Использование NSURLProtocol в Swift

08 октября 2015

Примечание по обновлению: данное руководство было обновлено для iOS 9 и Swift, проверено для Xcode 7.01.

NSURLProtocol - это как волшебный ключ к URL. Он позволяет предопределить, как работает система загрузки URL у Apple, определяя схемы пользовательских URL, и повторно переопределяя поведение существующих схем URL.

Звучит как волшебство? Так и должно быть, потому что URL словно любовь, которая везде вокруг нас. Что используют UIWebView и WKWebView? Конечно же URL. Что используется для потокового видео в MPMoviePlayer? И опять URL. Как вы отправляете кого-то на iTunes за вашим приложением, входите в FaceTime или Skype, загружаете приложение в систему, или, даже вставляете изображение в HTML файл? И снова с URL. Взгляните на NSFileManager и обратите внимание, как много из его методов, работающих с файлами, требуют и возвращают URL!

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

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

Итак, вы готовы начать изучение возможностей NSURLProtocol? Хорошо, устраивайтесь поудобнее с чашечкой чая и начнем, расширяющее сознание, обсуждение и пошаговые упражнения.

Начало

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

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

Шаги, через которые вы пройдете:

  1. использование UIWebView для отображения веб-сайтов;
  2. использование Core Data для кеширования результатов.

Если вы не знакомы с Core Data, то можете заглянуть в учебник. Однако, кода в этом уроке должно быть достаточно для того, чтобы понять возможности NSURLProtocol. Использование Core Data является простым способом реализации локального кеша, так что на этом этапе, вам этого будет достаточно.

Обзор исходного проекта

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

Когда вы откроете проект, то увидите два основных файла. Первый файл - Main.storyboard. Он имеет стандартный UIViewController, как раз такой, какой вам нужен для реализации. Обратите внимание на UITextField (для ввода URL), UIButton (для вызова веб-запросов) и UIWebView.

Откройте BrowserViewController.swift. Здесь вы увидите основное поведение, установленное для UI компонентов. UIViewController реализует протокол UITextFieldDelegate так, что вы можете отправлять запрос, когда пользователь нажимает клавишу возврата. IBAction для кнопки предварительно установлен на такое же поведение, как и клавиши возврата. Метод sendRequest() берет текст из текстового поля, создает объект NSURLRequest и вызывает метод loadRequest(_:)из UIWebView для загрузки.

После того, как вы ознакомитесь с приложением, запустите его! Когда оно откроется, введите "http://swiftbook.ru" и нажмите кнопку "Go". UIWebView загрузит ответ и отобразит результат в приложении - довольно просто для начала, правда? Пришло время размять мышцы на пальцах, приступаем к коду!

Перехват сетевых запросов

Набор классов, известных как URL Loading System (система загрузки URL), обрабатывает запросы URL на iOS. В центре системы загрузки URL находится NSURL класс. Для сетевых запросов этот класс говорит, какой именно хост (host) пытается достичь вашего приложения и указывает путь к ресурсу на этом хосте. Кроме того, объект NSURLRequest добавляет такую информацию, как: HTTP заголовки, тело вашего сообщения и т.д.. Система загрузки обеспечивает несколько различных классов, которые вы можете использовать, чтобы обработать запрос, наиболее распространенными из которых являются NSURLConnection и NSURLSession.

Теперь пришло время, чтобы начать перехват всех NSURLRequest, отправленных с приложения. Для этого вам нужно создать свою собственную реализацию NSURLProtocol.

Нажмите File\New\File…. Выберете iOS\Source\Cocoa Touch Class и затем нажмите кнопку Next. В поле Class введите MyURLProtocol, а в поле Subclass of введите NSURLProtocol. Убедитесь, что выбранный язык -Swift. Затем, нажмите Next и Create, когда появится диалоговое окно.

Откройте MyURLProtocol.swift и замените его содержимое на следующее:

import UIKit
 
var requestCount = 0

class MyURLProtocol: NSURLProtocol {
  override class func canInitWithRequest(request: NSURLRequest) -> Bool {
    print("Request #\(requestCount++): URL = \(request.URL!.absoluteString)")
    return false
  } 
}

Каждый раз, когда система загрузки URL получает запрос, что нужно загрузить URL, она ищет зарегистрированного обработчика протокола для обработки запроса. Каждый обработчик сообщает системе, может ли он справиться с данным запросом посредством canInitWithRequest(_:).

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

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

Если вы хотите реализовать новый протокол, такой как foo: //, то это то место, которое нужно проверить, чтобы увидеть, является ли запрошенная схема URL foo. Но в приведенном выше примере, вы просто возвращаете false, что говорит вам о том, вам ваше приложение не может обработать запрос. Просто подождите минуту и вы начнете их обрабатывать в ближайшее время!

Откройте AppDelegate.swift и замените application(_:didFinishLaunchingWithOptions:) следующим:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    NSURLProtocol.registerClass(MyURLProtocol)
    return true
}

Теперь, когда ваше приложение запущено, оно будет регистрировать протокол с системой загрузки URL. Это означает, что он будет иметь возможность обрабатывать каждый запрос, доставленный системе загрузки URL. Сюда мы включаем код, который вызывает систему загрузки напрямую, а также многие компоненты системы, которые полагаются на загрузочные фреймворки URL, такие как UIWebView.

Запустите проект. Вставьте http://swiftbook.ru как веб-сайт, нажмите на Go и проверьте консоль Xcode. Теперь на каждый запрос приложения, который необходимо выполнить, система загрузки URL запрашивает класс, если она может обработать его. В консоли вы должны увидеть что-то вроде этого:

В настоящее время, ваш класс только записывает строковое отображение URL запроса и возвращает false. Это означает, что ваш пользовательский класс не может обработать запрос. Но если вы посмотрите в логах, то увидите все запросы из UIWebView. Сюда включены и главный сайт (.html), и все эссеты (наборы), такие как изображения в формате JPEG и CSS файлы. Каждый раз, когда UIWebView нужно отправить запрос, он отображается в консоли, прежде чем сработать. Запросов будет целая куча - более пятисот - из-за разных наборов (эссетов) на веб-странице.

Итак, ваш пользовательский класс объявляется для каждого запроса URL, а в последующем вы сможете уже сделать что-то в каждом запросе!

Пользовательская загрузка URL

"Я люблю, когда страницы загружаются вечно" - никто так не скажет! Никогда! Теперь вы должны убедиться, что ваше приложение действительно может обрабатывать запросы. Как только true возвращается в canInitWithRequest(_:), на ваш класс переходит ответственность касательно обработки запроса. Это означает, что вы должны получить запрошенные данные и предоставить их обратно в систему загрузки URL.

Как вы получаете данные?

Если вы реализуете новый сетевой протокол приложения с нуля (например, добавление foo: // протокола), то вы познаете суровые радости реализации сетевого протокола приложения. Но так как наша цель просто вставить пользовательский слой кеширования, то вы можете просто получить данные с помощью NSURLConnection.

Фактически вы только собираетесь перехватить запрос и затем передать его обратно стандартной системе загрузки URL через использование NSURLConnection.

Ваш пользовательский подкласс NSURLProtocol возвращает данные через объект, который реализует протокол NSURLProtocolClient. Название немного сбивает с толку: NSURLProtocol - это класс, а NSURLProtocolClient - протокол!

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

Откройте MyURLProtocol.swift и добавьте следующее свойство в верхней части определения класса MyURLProtocol:

var connection: NSURLConnection!

Затем найдите canInitWithRequest(_:). Измените строчку возврата на true:

return true

Добавьте еще четыре метода:

override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
    return request
}
 
override class func requestIsCacheEquivalent(aRequest: NSURLRequest,
                                   toRequest bRequest: NSURLRequest) -> Bool {
    return super.requestIsCacheEquivalent(aRequest, toRequest:bRequest)
}
 
override func startLoading() {
    self.connection = NSURLConnection(request: self.request, delegate: self)
}
 
override func stopLoading() {
    if self.connection != nil {
        self.connection.cancel()
    }
    self.connection = nil
}

Вашему предстоит определять, что значит «канонический запрос» (взятый за образец) и как минимум, он должен вернуть такой же канонический запрос, для таких же входящих запросов. Таким образом, если в этом методе вводятся семантически равные (т.е. не обязательно ===) запросы, то выходные запросы должны быть тоже семантически равны. Например, если ваша пользовательская схема URL чувствительна к регистру, то вы можете решить, что все канонические URL будут написаны в строчном регистре.

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

requestIsCacheEquivalent(_:toRequest:) - место, где вы можете определить, когда два отдельных запроса пользовательской схемы URL (т.е. foo: //) равны, с точки зрения способности возможности кеширования. Если два запроса равны, то они должны использовать одни и те же данные кеша. Это касается и самой системы загрузки URL и встроенной системы кеширования, которую мы не рассматриваем в этом туториале. Таким образом, для данного упражнения вы просто полагаетесь на реализацию по умолчанию суперкласса.

Система загрузки использует startLoading() и stopLoading() для того, чтобы сказать NSURLProtocol, и начать или остановить обработку запроса. Ваша начальная реализация настраивает NSURLConnection для загрузки данных. Существует метод остановки, так что URL загрузка может быть отменена. В приведенном выше примере, как раз происходит такая обработка, отменяя текущее соединение и избавляясь от него.

Ю-ху! Вы реализовали интерфейс, необходимый для действующего экземпляра NSURLProtocol.

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

Откройте MyURLProtocol.swift и добавьте следующие методы:

func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
    self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
}
 
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
    self.client!.URLProtocol(self, didLoadData: data)
}
 
func connectionDidFinishLoading(connection: NSURLConnection!) {
    self.client!.URLProtocolDidFinishLoading(self)
}
 
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
    self.client!.URLProtocol(self, didFailWithError: error)
}

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

Таким образом, обработчик MyURLProtocol создает свое собственное подключение NSURLConnection и запрашивает его продолжить запрос. В ответных методах делегата NSURLConnection, описанных выше, обработчик протокола ретранслирует сообщения от соединения обратно, к системе загрузки URL. Эти сообщения говорят о прогрессе загрузки, о завершении операции и об ошибках.

Посмотрите и вы увидите сходство в сигнатурах сообщений NSURLConnectionDelegate и NSURLProtocolClient — оба они используют API для асинхронной загрузки данных. Также обратите внимание, как MyURLProtocol использует свойство client для отправки сообщений обратно в систему загрузки URL.

Запустите проект. Когда приложение откроется, введите тот же URL и нажмите «Go».

Ой-ой! Ваш браузер больше ничего не загружает!

Если вы посмотрите в панель дебаггера, то увидите, как растет число запросов одного и того же URL. Что же не так?

Сначала вернитесь в Xcode и остановите симулятор, пока у вас не начались более серьезные проблемы.

Разрыв бесконечного цикла запросов

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

Таким образом система загрузки URL создает экземпляр вашего протокола и вызывает startLoading. Затем ваша реализация создает и запускает свой NSURLConnection. Но помимо этого она также вызывает систему загрузки URL. Так как ваш метод canInitWithRequest(_:) возвращает true, то он создает новый экземпляр MyURLProtocol.

Очевидно, что вы не можете всегда возвращать true в методе canInitWithRequest(_:). Вам нужен своего рода контроль для того, чтобы вы могли сказать системе загрузки URL, чтобы она обрабатывала запрос единожды. Решение кроется в интерфейсе NSURLProtocol. Поищите метод класса setProperty(_:forKey:inRequest:), который позволяет добавлять дополнительные свойства в указанный URL. В этом случае, вы сможете установить «ярлык», прикрепив к запросу это свойство, таким образом браузер поймет, встречал ли он этот запрос раньше или нет.

Вот пример того, как вы можете вытащить ваш браузер из бесконечного цикла безумия: откройте NSURLProtocol.swift, измените методы startLoading() и canInitWithRequest(_:), вот так:

override class func canInitWithRequest(request: NSURLRequest) -> Bool {
    print("Request #\(requestCount++): URL = \(request.URL!.absoluteString)")
 
    if NSURLProtocol.propertyForKey("MyURLProtocolHandledKey", inRequest: request) != nil {
      return false
    }
 
    return true
}

override func startLoading() { 
    var newRequest = self.request.mutableCopy() as NSMutableURLRequest
    NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)
 
    self.connection = NSURLConnection(request: newRequest, delegate: self)
 }

Теперь startLoading() выставляет свойство связанное с ключом «MyURLProtocolHandledKey» на true для текущего запроса. Это означает, что в следующий раз, когда он вызывает метод canInitWithRequest(_:) для данного экземпляра NSURLProtocol, протокол может спросить, выставлено ли это свойство.

Если его значение выставлено на true, то это означает, что вам не нужно обрабатывать этот запрос еще раз, и система загрузки URL получит данные из сети. Так как экземпляр MyURLProtocol является делегатом для этого запроса, то он получит ответ от NSURLConnectionDelegate.

Запустите ваше приложение. Когда вы сделаете это, сразу убедитесь в том, что оно благополучно отображает все данные в вашем UIWebView. Сладкая победа! Теперь консоль выглядит примерно вот так:

Request #0: URL = http://swiftbook.ru/
Request #1: URL = http://swiftbook.ru/
Request #2: URL = http://swiftbook.ru/
Request #3: URL = http://swiftbook.ru/
Request #4: URL = http://swiftbook.ru/
Request #5: URL = http://swiftbook.ru/
Request #6: URL = http://swiftbook.ru/
Request #7: URL = http://swiftbook.ru/
Request #8: URL = http://swiftbook.ru/
Request #9: URL = http://swiftbook.ru/
Request #10: URL = http://swiftbook.ru/
Request #11: URL = http://swiftbook.ru/
Request #12: URL = http://swiftbook.ru/
Request #13: URL = http://swiftbook.ru/
Request #14: URL = http://swiftbook.ru/
Request #15: URL = http://swiftbook.ru/wp-content/uploads/img_from_drupal_swiftbook/files/css/css_lQaZfjVpwP_oGNqdtWCSpJT1EMqXdMiU84ekLLxQnc4.css
Request #16: URL = http://swiftbook.ru/wp-content/uploads/img_from_drupal_swiftbook/files/css/css_lQaZfjVpwP_oGNqdtWCSpJT1EMqXdMiU84ekLLxQnc4.css 
Request #17: URL = http://swiftbook.ru/wp-content/uploads/img_from_drupal_swiftbook/files/css/css_lQaZfjVpwP_oGNqdtWCSpJT1EMqXdMiU84ekLLxQnc4.css 
Request #18: URL = http://swiftbook.ru/wp-content/uploads/img_from_drupal_swiftbook/files/css/css_lQaZfjVpwP_oGNqdtWCSpJT1EMqXdMiU84ekLLxQnc4.css 
Request #19: URL = http://swiftbook.ru/wp-content/uploads/img_from_drupal_swiftbook/files/css/css_0rdNKyAx9df4tRKovKjnVDsGvZwVuYAKO-m17AAJ1Dk.css ..

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

Реализация локального кеша

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

Заметка:

Исходный проект уже включает в себя базовую модель Core Data и стек. Вам не нужно знать детали Core Data, думайте о нем как о хранилище данных.

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

import CoreData

Затем добавьте следующие свойства в реализацию класса:

var mutableData: NSMutableData!
var response: NSURLResponse!

Свойство response будет держать ссылку на метаданные, которые вам понадобятся для хранения ответа от сервера. Свойство mutableData будет использовано для хранения данных, которые получает соединение в методе делегата connection(_:didRecieveData).

Добавьте следующий метод в класс:

func saveCachedResponse () {
    print("Saving cached response")
 
    // 1
    let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
    let context = delegate.managedObjectContext
 
    // 2
    let cachedResponse = NSEntityDescription.insertNewObjectForEntityForName("CachedURLResponse", inManagedObjectContext: context) as NSManagedObject
 
    cachedResponse.setValue(self.mutableData, forKey: "data")
    cachedResponse.setValue(self.request.URL!.absoluteString, forKey: "url")
    cachedResponse.setValue(NSDate(), forKey: "timestamp")
    cachedResponse.setValue(self.response.MIMEType, forKey: "mimeType")
    cachedResponse.setValue(self.response.textEncodingName, forKey: "encoding")
 
    // 3
    if context.hasChanges {
      do {
        try context.save()
      } catch let error as NSError {
        NSLog("произошла ошибка \(error), \(error.userInfo)")
        abort()
      }
    }

Вот что делает этот метод:

  1. Получает NSManagedObjectContext из Core Data экземпляра AppDelegate. Этот контекст является вашим интерфейсом взаимодействия с Core Data.
  2. Создаем экземпляр NSManagedObject для соответствия модели, которую вы видели в фйле .xcdatamodeld. Устанавливаем значения, основываясь на ссылках на NSURLResponse и NSMutableData, которые у вас есть.
  3. Сохраняем контекст управляемого объекта Core Data, если в нем произошли изменения.

Теперь у вас есть возможность сохранения данных, осталось только вызвать этот метод. Продолжаем работать в MyURLProtocol.swift. Измените методы делегата NSURLConnection на следующие:

func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
    self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
 
    self.response = response
    self.mutableData = NSMutableData()
}
 
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
    self.client!.URLProtocol(self, didLoadData: data)
    self.mutableData.appendData(data)
}
 
func connectionDidFinishLoading(connection: NSURLConnection!) {
    self.client!.URLProtocolDidFinishLoading(self)
    self.saveCachedResponse()
}

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

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

Получение закешированного ответа

Наконец настало время получить ранее закешированные запросы и отправить их клиенту NSURLProtocol. Откройте MyURLProtocol.swift и добавьте следующий метод:

func cachedResponseForCurrentRequest() -> NSManagedObject? {
    // 1
    let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
    let context = delegate.managedObjectContext
 
    // 2
    let fetchRequest = NSFetchRequest()
    let entity = NSEntityDescription.entityForName("CachedURLResponse", inManagedObjectContext: context)
    fetchRequest.entity = entity
 
    // 3
    let predicate = NSPredicate(format:"url == %@", self.request.URL!.absoluteString)
    fetchRequest.predicate = predicate
 
    // 4
    do {
      let possibleResult = try context.executeFetchRequest(fetchRequest)
      
      if !possibleResult.isEmpty {
        return possibleResult.first as? NSManagedObject
      }
      
    } catch let error as NSError {
      print("Fetch failed: \(error.localizedDescription)")
    }
    
    return nil
}

Вот что он делает:

  1. Берет контекст управляемого объекта Core Data, точно так же как и в saveCachedResponse().
  2. Создает NSFetchRequest, говоря о том, что вы хотите найти все сущности с именем CachedURLResponse. Это и есть та сущность в модели управляемого объекта, которую вы хотите получить.
  3. Предикат для вашего запроса (fetch request) должен получить объект CachedURLResponse, который имеет отношение к URL и который вы пытаетесь загрузить. Как раз этот блок, это и делает.
  4. Исполняет запрос и проверяет на наличие результатов. Если массив не пуст, то возвращается первый из них.

Теперь нам нужно вернуться и посмотреть на реализацию startLoading(). Вместо того, чтобы все подряд загружать из сети, нужно сделать так, чтобы он сначала проверял наличие закешированного ответа по URL. Найдите соответствующую реализацию и замените ее на следующую:

override func startLoading() {
    // 1
    let possibleCachedResponse = self.cachedResponseForCurrentRequest()
    if let cachedResponse = possibleCachedResponse {
        print("Serving response from cache")
 
        // 2
        let data = cachedResponse.valueForKey("data") as! NSData!
        let mimeType = cachedResponse.valueForKey("mimeType") as! String!
        let encoding = cachedResponse.valueForKey("encoding") as! String!
 
        // 3
        let response = NSURLResponse(URL: self.request.URL!, MIMEType: mimeType, expectedContentLength: data.length, textEncodingName: encoding)
 
        // 4
        self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
        self.client!.URLProtocol(self, didLoadData: data)
        self.client!.URLProtocolDidFinishLoading(self)
    } else {

        // 5
        print("Serving response from NSURLConnection")
 
        let newRequest = self.request.mutableCopy() as! NSMutableURLRequest
        NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)
        self.connection = NSURLConnection(request: newRequest, delegate: self)
    }
}

Вот, что здесь происходит:

  1. Сначала вам нужно выяснить, есть ли закешированный ответ на текущий запрос.
  2. Если есть, то мы получаем соответствующие данные из кеша.
  3. Создаем объект NSURLResponse из сохраненных данных.
  4. Говорим о данных в ответе клиенту. Устанавливаем политику конфиденциальности на .NotAllowed, так как мы не хотим, чтобы клиент кешировал что-либо с тех самых пор, как это стало вашей обязанностью. Затем вызываете URLProtocolDidFinishLoading для того, чтобы подать сигнал о завершении загрузки. Никаких сетевых запросов, так то!
  5. Если ничего в кеше по текущему запросу нет, то мы просто загружаем данные как обычно.

Запустите ваш проект снова. Зайдите на парочку сайтов и выйдете из приложения. Установите режим работы вашего устройства на «в самолете» (или если вы работаете на симуляторе, то вам нужно выключить wi-fi вашего компьютера или просто выдерните кабель Ethernet) и снова запустите проект. Попробуйте вновь зайти на те же сайты, которые вы только что посещали. Ваши странички должны подгрузиться из кeша!

В консоли вы увидите множество сущностей следующего типа:

Request #7: URL = http://swiftbook.ru/
Serving response from cache

Это лог того, что ваш ответ поступает из кeша.

Ну вот и все! Ваше приложение благополучно кеширует данные и со скоростью света выдает их оттуда при повторном запросе! Ваши пользователи будут просто счастливы пользоваться вашим приложением у себя на устройстве!

Когда нужно использовать NSURLProtocol?

Как вы можете использовать NSURLProtocol, чтобы сделать ваше приложение лучше, быстрее, сильнее и зубодробительно классным? Вот вам несколько примеров:

Обеспечение индивидуальных ответов для сетевых запросов:

Неважно, делаете ли вы запрос с использованием UIWebView, NSURLConnection или используете стороннюю библиотеку (типа AFTNetworking, MKNetworkKit..., так как все они надстроены на NSURLConnection). Вы можете создать индивидуальный ответ и для метаданных, и для данных. Вы можете использовать его заглушить ответ на запрос при тестировании, например.

Пропустить активность сети и предоставить локальные данные:

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

Перенаправление сетевых запросов:

Хотелось ли вам когда-нибудь перенаправить запрос на proxy сервер без того, чтобы доверить пользователю изменить какие-либо настройки iOS? Что ж, вы можете! NSURLProtocol предоставляет то, что вам нужно — контроль над запросами. Вы можете настроить свое приложение так, что оно будет перехватывать и перенаправлять запросы на другой прокси-сервер или туда, куда вам только захочется. Речь идет о контроле запроса!

Изменение user-agent вашего запроса:

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

Используйте свой собственный сетевой протокол:

Вы можете иметь свой собственный сетевой протокол (например, что-либо построенное на UDP). Вы можете реализовать это в вашем приложении, что так же не ограничивает ваши возможности в использовании любой сетевой библиотеки.

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

Конечный вариант проекта вы можете скачать здесь!

 

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

За основу взят урок: http://www.raywenderlich.com/76735/using-nsurlprotocol-swift

Содержание