Туториал по Alamofire

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

Xcode: 
7.3.1
Swift: 
2.2

Alamofire - это HTTP сетевая библиотека на Swift для iOS и Mac OS X. Она обеспечивает элеганстный интерфейс сетевого стека Foundation от Apple, упрощающего ряд общих сетевых задач.

Alamofire предоставляет цепочку методов ответ/запрос, параметр JSON и ответ сериализации, аутентификацию и многие другие функции. В этом туториале по Alamofire вы будете использовать Alamofire для выполнения основных сетевых задач, таких как загрузка файлов и запрос данных со сторонних RESTful API.

Alamofire элегантен, потому что написан с нуля на Swift и не наследуюет ничего от AFNetworking, его аналога в Objective-C.

Вы должны иметь общее понимание о HTTP и некоторое представление о сетевых классах Apple, таких как NSURLSession и NSURLConnection.

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

Поехали!

Загрузите стартовый проект здесь.

Приложение для туториала по Alamofire называется PhotoTagger; после установки он позволит вам выбрать изображение из библиотеки (или с камеры, если вы работаете на реальном устройстве) и загрузить изображение на сторонний сервис, который выполнит задачи по распознаванию образов, чтобы они отвечали требованиям списка тегов и основным цветам изображения:

Запустите проект в Xcode, и вы увидите следующее:

Нажмите Select Photo и выберите фотографию. Фоновое изображение будет заменено на изображение, которое вы выбрали.

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

Imagga API

Imagga - это Platform-as-a-Service для распознавания изображений, которая предоставляет расстановку тегов изображений API для разработчиков и фирм для создания приложений с масштабируемыми яркими облачными изображениями. Вы можете поиграть с их демо версией сервиса по автоматической расстановке тегов здесь.

Вам нужно создать бесплатную учетную запись разработчика через Imagga для этого туториала по Alamofire. Imagga требует заголовок авторизации в каждом запросе HTTP, поэтому только люди с учетной записью могут пользоваться их сервисом. Перейдите по ссылке https://imagga.com/auth/signup/hacker и заполните форму. После создания учетной записи, проверьте панель:

Секретный токен, указазнный в разделе Authorization, пригодится вам позже. Эту информацию нужно включать при каждом запросе HTTP в качестве заголовка.

Заметка

 

Убедитесь, что вы скопировали секретный токен полностью, для этого вам нужно пролистнуть вправо.

Вы будете использовать конечную точку контента Imagga для загрузки фотографий, конечную точку расстановки тегов для распознавания изображений и конечную точку цвета для идентификации цвета. Вы можете прочитать все о API Imagga на http://docs.imagga.com.

Установка зависимостей

Создайте файл с именем Podfile в главной директории проекта со следующим содержимым:

Затем установите зависимости CocoaPods на pod install. Если вы еще не установили CocoaPods на свой компьютер ранее, начните с туториала Как использовать CocoaPods в Swift.

platform :ios, '9.0'
 
inhibit_all_warnings!
use_frameworks!
 
target 'PhotoTagger' do
  pod 'Alamofire', '~> 3.1.2'
end

Закройте проект и откройте созданный PhotoTagger.xcworkspace. Запустите проект - никаких визуальных изменений в управлении приложения вы заметить не должны . Так и должно быть - ваша следующая задача состоит в том, чтобы добавить некоторые HTTP вызовы в сервис RESTful, чтобы получить JSON.

REST, HTTP, JSON - Что это такое?

Если до этого туториала по Alamofire вы еще не сталкивались с использованием сторонних сервисов Интернета, вы можете не знать, что означают все эти аббревиатуры! :]

HTTP является протоколом приложения, или набором правил, которые используют веб-сайты для передачи данных с веб-сервера на ваш экран. Вы встречали HTTP (или HTTPS) в передней части каждого URL, который вы вводите в веб-браузере. Возможно, вы слышали о других протоколах приложений, таких как FTP, Telnet и SSH. HTTP определяет несколько методов запроса или глаголы, которые клиент (ваш веб-браузер или приложение) использует, для того чтобы указать нужное действие:

GET: Используется для извлечения данных, таких как веб-страницы, но не изменяет какие-либо данные на сервере.

HEAD: Идентично GET, но только обратно отправляет заголовки, а не фактическую информацию.

POST: Используется для отправки данных на сервер, обычно при заполнении формы и нажатии кнопки submit (передачи данных).

PUT: Используется для отправки данных в определенное придусмотренное место.

DELETE: Удаляет данные из указанного придусмотренного места.

REST или REpresentational State Transfer, представляет собой набор правил для создания последовательных, простых в использовании и обслуживании веб-API. У REST есть несколько правил архитектуры, которые обеспечивает принудительное использование таких вещей, как неустойчивые состояния между запросами, создание кэшируемых запросов и обеспечение одинаковых интерфейсов. Все это дает разработчикам приложений, таким как вы, возможность легко интегрировать API в ваши приложения - без необходимости отслеживать состояние данных по запросам.

JSON - это JavaScript Object Notation, обеспечивает простой, удобный для чтения и перенесения механизм передачи данных между двумя системами. JSON имеет ограниченное количество типов данных: строка, boolean, массив, объект/словарь, null (0) и число. Различия между целыми числами и десятичными нет. Apple предоставляет класс NSJSONSerialization для того, чтобы помочь конвертировать ваши объекты из памяти в формат JSON и наоборот.

Сочетание HTTP, REST и JSON - это большая часть веб-сервисов, доступных для вас в качестве разработчика. Попытнка понять, как работает каждый элемент в отдельности- может стать огромной задачей. Библиотеки, такие как Alamofire, могут помочь снизить сложность взаимодействия с этими сервисами - и с их помощью вы освоите их и будете работать быстрее.

А чем хорош Alamofire?

А зачем вообще вам может быть нужен Alamofire? Apple уже предоставляет NSURLSession и другие классы для загрузки содержимого через HTTP, так зачем все усложнять привлекая стороннюю библиотеку?

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

Есть несколько основных функций, доступных в Alamofire:

  • .upload:  Выгрузка (uploading) файлов с помощью многокомпонентных, поточных, файловых методов и методов данных.
  • .download: Загрузка файлов или возобновление загрузки, находящейся в процессе.
  • .request: Любой другой запрос HTTP, несвязанный с передачей файлов.

Эти функции Alamofire находятся в области видимости модуля, а не класса или структуры. В основе Alamofire стоят классы и структуры, такие как Manager, Request и Response; тем не менее, вам не нужно полностью понимать структуру Alamofire, чтобы начать ей пользоваться.

Вот пример работы одной и той же сетевой операции через NSURLSession от Apple и функцию request в Alamofire:

// With NSURLSession
public func fetchAllRooms(completion: ([RemoteRoom]?) -> Void) {
  let url = NSURL(string: "http://localhost:5984/rooms/_all_docs?include_docs=true")!
 
  let urlRequest = NSMutableURLRequest(
    URL: url,
    cachePolicy: .ReloadIgnoringLocalAndRemoteCacheData,
    timeoutInterval: 10.0 * 1000)
  urlRequest.HTTPMethod = "GET"
  urlRequest.addValue("application/json", forHTTPHeaderField: "Accept")
 
  let task = urlSession.dataTaskWithRequest(urlRequest)
    { (data, response, error) -> Void in
    guard error == nil else {
      print("Error while fetching remote rooms: \(error)")
      completion(nil)
      return
    }
 
    guard let json = try? NSJSONSerialization.JSONObjectWithData(data!,
      options: []) as? [String: AnyObject] else {
        print("Nil data received from fetchAllRooms service")
        completion(nil)
        return
    }
 
    guard let rows = json["rows"] as? [[String: AnyObject]] {
      print("Malformed data received from fetchAllRooms service")
      completion(nil)
      return
    }
 
    var rooms = [RemoteRoom]()
    for roomDict in rows {
      rooms.append(RemoteRoom(jsonData: roomDict))
    }
 
    completion(rooms)
  }
 
  task.resume()
}

Против:

// With Alamofire
func fetchAllRooms(completion: ([RemoteRoom]?) -> Void) {
  Alamofire.request(
    .GET,
    "http://localhost:5984/rooms/_all_docs",
    parameters: ["include_docs": "true"],
    encoding: .URL)
    .validate()
    .responseJSON { (response) -> Void in
      guard response.result.isSuccess else {
        print("Error while fetching remote rooms: \(response.result.error)")
        completion(nil)
        return
      }
 
      guard let value = response.result.value as? [String: AnyObject],
        rows = value["rows"] as? [[String: AnyObject]] else {
          print("Malformed data received from fetchAllRooms service")
           completion(nil)
           return
      }
 
      var rooms = [RemoteRoom]()
      for roomDict in rows {
        rooms.append(RemoteRoom(jsonData: roomDict))
      }
 
      completion(rooms)
  }
}

Вы можете увидеть, что вариант для Alamofire короче и яснее, в сравнении с функцией. Вы преобразовали ответ с responseJSON(options:completionHandler:) и вызов validate() на объект Response упрощает обработку ошибки.

Выгрузка файлов 

Откройте ViewController.swift и добавьте следующее расширение класса в конец файла:

// Networking calls
extension ViewController {
  func uploadImage(image: UIImage, progress: (percent: Float) -> Void,
    completion: (tags: [String], colors: [PhotoColor]) -> Void) {
    guard let imageData = UIImageJPEGRepresentation(image, 0.5) else {
      print("Could not get JPEG representation of UIImage")
      return
    }
  }
}

Первый шаг в выгрузке изображения на Imagga - это получение правильного формата для API. Выше, Image Picker API возвращает экземпляр UIImage, который вы преобразуете в JPEG экземпляр NSData.

Затем перейдите к imagePickerController(_:didFinishPickingMediaWithInfo:) и добавьте следующую строку сразу после установки изображения на imageView:

// 1
takePictureButton.hidden = true
progressView.progress = 0.0
progressView.hidden = false
activityIndicatorView.startAnimating()
 
uploadImage(
  image,
  progress: { [unowned self] percent in
    // 2
    self.progressView.setProgress(percent, animated: true)
  },
  completion: { [unowned self] tags, colors in
    // 3
    self.takePictureButton.hidden = false
    self.progressView.hidden = true
    self.activityIndicatorView.stopAnimating()
 
    self.tags = tags
    self.colors = colors
 
    // 4
    self.performSegueWithIdentifier("ShowResults", sender: self)
})

Все, что связано с Alamofire является асинхронным, что означает, что вы будете обновлять пользовательский интерфейс в асинхронном режиме:

  1. Скройте кнопку загрузки, и покажите progress view и activity view.
  2. В то время как файл выгружается, вы вызываете обработчик состояния с обновленным состоянием в процентах. Это видоизменяет вид строки состяния загрузки.
  3. Обработчик завершения  сработает в тот момент, когда закончится загрузка. Это вернет состояние на первоначальное значение.
  4. Наконец Storyboard переходит к экрану с результатами успешной (или неудачной) загрузки. Пользовательский интерфейс не меняется, основываясь на состояние ошибки.

Затем добавьте следующую строку в верхнюю часть ViewController.swift: :

import Alamofire

Это позволит вам использовать функциональные возможности, предоставляемые модулем Alamofire в вашем коде.

Затем вернитесь к uploadImage(_:progress:completion:) и добавьте следующую строчку после того места, где вы преобразовывали экземпляр UIImage:

Alamofire.upload(
  .POST,
  "http://api.imagga.com/v1/content",
  headers: ["Authorization" : "Basic xxx"],
  multipartFormData: { multipartFormData in
    multipartFormData.appendBodyPart(data: imageData, name: "imagefile",
      fileName: "image.jpg", mimeType: "image/jpeg")
  },
  encodingCompletion: { encodingResult in
  }
)

Убедитесь, что заменили Basic xxx фактическим заголовком авторизации, взятым с панели Imagga. Здесь вы преобразовали двоичный объект данных в формате JPEG(imageData) в составной запрос MIME для конечной точки контента Imagga.

Затем добавьте следующую строчку к замыканию encodingCompletion:

switch encodingResult {
case .Success(let upload, _, _):
  upload.progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
    dispatch_async(dispatch_get_main_queue()) {
      let percent = (Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
      progress(percent: percent)
    }
  }
  upload.validate()
  upload.responseJSON { response in
  }
case .Failure(let encodingError):
  print(encodingError)
}

Этот фрагмент кода вызывает функцию Alamofire upload и легковесное вычисление (small calculation) передается для обновления строки прогресса в момент выгрузки файлов. Обратите внимание, что отображение прогресса происходит в блоке dispatch главной очереди, так как вы обновляете пользовательский интерфейс.

Заметка

 

Alamofire не гарантирует обратный вызов о состоянии выгрузки в основной очереди; вам необходимо удостовериться, что обновление пользовательского интерфейса будет направлено в блоке dispatch основной очереди. Некоторые обратные вызововы Alamofire, такие как responseJSON, вызываются в главной очереди по умолчанию. Например:


dispatch_async(queue ?? dispatch_get_main_queue()) {
  let response = ...
  completionHandler(response)
}

Чтобы изменить это дефолтное поведение, вам нужно применить dispatch_queue_t  к Alamofire.

Затем добавьте следующий код к upload.responseJSON: :

// 1.
guard response.result.isSuccess else {
  print("Error while uploading file: \(response.result.error)")
  completion(tags: [String](), colors: [PhotoColor]())
  return
}
// 2.
guard let responseJSON = response.result.value as? [String: AnyObject],
  uploadedFiles = responseJSON["uploaded"] as? [AnyObject],
  firstFile = uploadedFiles.first as? [String: AnyObject],
  firstFileID = firstFile["id"] as? String else {
    print("Invalid information received from service")
    completion(tags: [String](), colors: [PhotoColor]())
    return
}
 
print("Content uploaded with ID: \(firstFileID)")
// 3.
completion(tags: [String](), colors: [PhotoColor]())

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

  1. Проверьте, был ли успешным ответ. Если нет, то выведите сообщение об ошибке и вызовите завершающий обработчик.
  2. Проверьте каждую часть ответа, проверьте соответствует ли ожидаемый тип фактическому полученному тип. Извлеките firstFileID из ответа. Если firstFileID не может быть получен, выведите сообщение об ошибке и вызовите завершающий обработчик.
  3. Вызовите завершающий обработчик для обновления пользовательского интерфейса. На данный момент у вас нет загруженных тегов или цветов, так что просто вызовите его с пустыми данными.

Заметка

 

Каждый ответ имеет энум Result со значением и типом. Используя автоматическую проверку, результат будет считаться успешным, если он возвращает валидный HTTP код между 200 и 299 и тип содержимого является валидным типом, указанным в поле заголовка HTTP Accept.

Вы можете выполнить ручную проверку, добавив опции .validate, как показано ниже:

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
  .validate(statusCode: 200..<300)
  .validate(contentType: ["application/json"])
  .response { response in
    // response handling code
}

Пользовательский интерфейс не будет отображать сообщение об ошибке, если она встретится (?) во время загрузки; просто не будут возвращены теги или цвета пользователю. Это не самый ваш лучший опыт, но для этого туториала по Alamofire это нормально.

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

Вы успешно выгрузили файл через Interwebs!

Получение данных

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

Добавьте следующий метод расширению ViewController ниже uploadImage(_:progress:completion:)

func downloadTags(contentID: String, completion: ([String]) -> Void) {
  Alamofire.request(
    .GET,
    "http://api.imagga.com/v1/tagging",
    parameters: ["content": contentID],
    headers: ["Authorization" : "Basic xxx"]
    )
    .responseJSON { response in
      guard response.result.isSuccess else {
        print("Error while fetching tags: \(response.result.error)")
        completion([String]())
        return
      }
 
      guard let responseJSON = response.result.value as? [String: AnyObject] else {
         print("Invalid tag information received from service")
         completion([String]())
         return
      }
      print(responseJSON)
      completion([String]())
  }
}

Не забудьте снова заменить Basic xxx фактическим заголовком авторизации. Здесь вы выполняете запрос HTTP GET конечной точки tagging , отправляя параметр URL content с идентификатором, который вы получили после выгрузки.

Затем вернитесь к uploadImage(_:progress:completion:) и замените вызов завершающий обработчик, при благополучном исходе загрузки, следующим:

self.downloadTags(firstFileID) { tags in
  completion(tags: tags, colors: [PhotoColor]())
}

Это просто отправляет ключевые слова завершающему обработчику.

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

В этом туториале по Alamofire вам не важна достоверность чисел, важен только массив названий тегов.

Затем вернитесь к downloadTags(_:completion:) и замените код внутри .responseJSON следующим:

// 1.
guard response.result.isSuccess else {
  print("Error while fetching tags: \(response.result.error)")
  completion([String]())
  return
}
 
// 2.
guard let responseJSON = response.result.value as? [String: AnyObject],
  results = responseJSON["results"] as? [AnyObject],
  firstResult = results.first,
  tagsAndConfidences = firstResult["tags"] as? [[String: AnyObject]] else {
    print("Invalid tag information received from the service")
    completion([String]())
    return
}
 
// 3.
let tags = tagsAndConfidences.flatMap({ dict in
  return dict["tag"] as? String
})
 
// 4.
completion(tags)

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

  1. Проверьте, успешен ли ответ. Если нет, то выведите сообщение об ошибке и вызовите завершающий обработчик.
  2. Проверьте каждую часть ответа, сверяя является ли ожидаемый тип фактическим полученным типом. Получите информацию tagsAndConfidences из ответа. Если tagsAndConfidences не может быть получен, выведите сообщение об ошибке и вызовите завершающий обработчик.
  3. Переберите каждый объект словаря в массиве tagsAndConfidences, извлекая значение, связанное с ключом tag.
  4. Вызовите завершающий обработчик, проходящего по tags, полученного от сервиса.

Заметка

Вы используете метод Swift flatMap для перебора каждого словаря в массиве tagsAndConfidences. Этот метод обрабатывает значения nil без сбоев и удаляет эти значения из возвращаемого результата. Это позволяет использовать условное извлечение (as?) для того, чтобы проверить, может ли значение словаря быть преобразовано в String.

Запустите снова проект, выберите фотографию, и вы увидите следующее окно:

Хитро! А у этого Immaga умный API. :] Теперь вы будете “выдергивать” цвета изображения.

Добавьте следующий метод расширению downloadTags(_:completion:)

func downloadColors(contentID: String, completion: ([PhotoColor]) -> Void) {
  Alamofire.request(
    .GET,
    "http://api.imagga.com/v1/colors",
    parameters: ["content": contentID, "extract_object_colors": NSNumber(int: 0)],
    // 1.
    headers: ["Authorization" : "Basic xxx"]
    )
    .responseJSON { response in
      // 2.
      guard response.result.isSuccess else {
        print("Error while fetching colors: \(response.result.error)")
        completion([PhotoColor]())
        return
      }
 
      // 3.
      guard let responseJSON = response.result.value as? [String: AnyObject],
        results = responseJSON["results"] as? [AnyObject],
        firstResult = results.first as? [String: AnyObject],
        info = firstResult["info"] as? [String: AnyObject],
        imageColors = info["image_colors"] as? [[String: AnyObject]] else {
          print("Invalid color information received from service")
          completion([PhotoColor]())
          return
      }
 
      // 4.
      let photoColors = imageColors.flatMap({ (dict) -> PhotoColor? in
        guard let r = dict["r"] as? String,
          g = dict["g"] as? String,
          b = dict["b"] as? String,
          closestPaletteColor = dict["closest_palette_color"] as? String else {
            return nil
        }
        return PhotoColor(red: Int(r),
          green: Int(g),
          blue: Int(b),
          colorName: closestPaletteColor)
      })
 
      // 5.
      completion(photoColors)
  }
}

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

  1. Не забудьте заменить Basic xxx фактическим заголовком авторизации.
  2. Убедитесь в том, что ответ прошел успешно, если нет, то выведите сообщение об ошибке и вызовите завершающий обработчик.
  3. Проверьте каждую часть ответа, сверяя является ли ожидаемый тип фактическим полученным типом. Получите из ответа информацию imageColors. Если imageColors  не может быть получен, выведите сообщение об ошибке и вызовите завершающий обработчик.
  4. Используйте снова flatMap. Вы перебираете возвращенные imageColors, изменяя данные в объекты PhotoColor, которые группируют цвета в формате RGB с именами цветов в виде строки. Обратите внимание, что при предоставленное замыкание всегда позволяет возвращать nil, так как flatMap будет их просто игнорировать.
  5. Вызовите завершающий обработчик, передавая в него photoColors.

И, наконец, вернитесь к uploadImage(_:progress:completion:) и замените вызов обработчика завершения, в случае успеха, следующим:

self.downloadTags(firstFileID) { tags in
  self.downloadColors(firstFileID) { colors in
    completion(tags: tags, colors: colors)
  }
}

Это вводит операцию по выгрузке изображения, загрузке тегов и загрузки цветов.

Запустите проект снова. На этот раз, вы должны увидеть возвращенные теги цветов:

Вы используете RGB цвета, записывая их в структурах PhotoColor для изменения фонового цвета вью. Теперь вы успешно выгрузили изображение в Imagga и получили данные из двух различных конечных точек. Большой путь уже проделан - но есть вариант, как можно улучшить использование PhotoTagger в Alamofire.

Улучшаем PhotoTagger

Вы, наверное, заметили, что есть повторяющийся код в PhotoTagger. Если Imagga выпустил v2 их API и v1 устарел, то PhotoTagger больше не будет функционировать, и вам будет нужно обновить URL в каждом из этих трех методов. Аналогичным образом, если ваш токен авторизации изменился, то вам будет нужно обновить его повсюду.

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

Создайте новый файл File\New\File… и выберите Swift file под iOS. Затем Next, назовите файл ImaggaRouter.swift, выберите группу PhotoTagger с желтой иконкой в виде папки и нажмите Create.

Замените содержимое нового файла со следующим:

import Foundation
import Alamofire
 
public enum ImaggaRouter: URLRequestConvertible {
  static let baseURLPath = "http://api.imagga.com/v1"
  static let authenticationToken = "Basic xxx"
 
  case Content
  case Tags(String)
  case Colors(String)
 
  public var URLRequest: NSMutableURLRequest {
    let result: (path: String, method: Alamofire.Method, parameters: [String: AnyObject]) = {
      switch self {
      case .Content:
        return ("/content", .POST, [String: AnyObject]())
      case .Tags(let contentID):
        let params = [ "content" : contentID ]
        return ("/tagging", .GET, params)
      case .Colors(let contentID):
        let params = [ "content" : contentID, "extract_object_colors" : NSNumber(int: 0) ]
        return ("/colors", .GET, params)
      }
    }()
 
    let URL = NSURL(string: ImaggaRouter.baseURLPath)!
    let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
    URLRequest.HTTPMethod = result.method.rawValue
    URLRequest.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization")
    URLRequest.timeoutInterval = NSTimeInterval(10 * 1000)
 
    let encoding = Alamofire.ParameterEncoding.URL
 
    return encoding.encode(URLRequest, parameters: result.parameters).0
  }
}

Заменить Basic xxx фактическим заголовком авторизации. Этот маршрутизатор помогает создавать экземпляры NSMutableURLRequest, предоставляя ему один из трех кейсов:  .Content.Tags(String), или .Colors(String). Теперь весь ваш шаблонный код находится в одном месте, если он вам когда-нибудь понадобится, чтобы обновить его.

Вернитесь к uploadImage(_:progress:completion:) и замените начало вызова Alamofire.upload следующим:

Alamofire.upload(
  ImaggaRouter.Content,
  multipartFormData: { multipartFormData in
    multipartFormData.appendBodyPart(data: imageData, name: "imagefile",
      fileName: "image.jpg", mimeType: "image/jpeg")
},
/// original code continues...

Далее замените вызов для  Alamofire.request в downloadTags(_:completion:) на:

Alamofire.request(ImaggaRouter.Tags(contentID))

И, наконец, обновите вызов  Alamofire.request в downloadColors(_:completion:) на:

Alamofire.request(ImaggaRouter.Colors(contentID))

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

А что дальше?

Конечный проект вы можете скачать тут.

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

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

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