Забудьте об Alamofire, вы можете обрабатывать JSON в Swift без зависимости.

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

Чтобы подчеркнуть это, существует несколько проектов с открытым исходным кодом, которые обеспечивают легкий доступ для синтаксического анализа данных JSON в более удобную для восприятия модель данных в Swift. Самым популярным из них, пожалуй, является SwiftyJSON.

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

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

Проект

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

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

Для начала откройте Xcode и создайте новую пустую площадку. Назовите его JSON.playground и сохраните в подходящем месте.

Мы собираемся создать несколько файлов, которые будем использовать для организации нашей работы, поэтому, как только игровая площадка будет открыта, откройте навигатор проекта, нажав ⌘1 или щелкнув меню View -> Navigators -> Show Project Navigator.

Теперь вы должны увидеть свой основной файл игровой площадки, а также папки с источниками и ресурсами.

Данные

Прежде чем писать какой-либо код, давайте посмотрим на данные, которые мы будем использовать. Существует множество бесплатных открытых конечных точек данных JSON, которые мы можем использовать для обучения.

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

Мы будем использовать конечную точку users, потому что она содержит вложенный JSON, который создает некоторые трудности, если вы новичок в использовании JSON, но очень распространен и является одной из причин, по которой платформы с открытым исходным кодом для работы с JSON в Swift стали настолько популярными.

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

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

[{
   “id”: 1,
   “name”: “Leanne Graham”,
   “username”: “Bret”,
   “email”: “[email protected]”,
   “address”: {
      “street”: “Kulas Light”,
      “suite”: “Apt. 556”,
      “city”: “Gwenborough”,
      “zipcode”: “92998–3874”,
      “geo”: {
         “lat”: “-37.3159”,
         “lng”: “81.1496”
      }
   },
   “phone”: “1–770–736–8031 x56442”,
   “website”: “hildegard.org”,
   “company”: {
      “name”: “Romaguera-Crona”,
      “catchPhrase”: “Multi-layered client-server neural-net”,
      “bs”: “harness real-time e-markets”
   }
},
…
]

Таких пользовательских объектов десять, но наш код будет обрабатывать любое количество.

В этих объектах интересно то, что у нас есть два вложенных объекта: address и company. Объект address также имеет вложенный объект с именем geo, но использование и доступ к его данным будет тривиальным.

Базовая загрузка

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

Создайте новый файл, щелкнув папку источников в навигаторе проекта и набрав ⌘N. Измените имя файла с New File.swift на Networking.swift.

Теперь в редакторе вы не должны видеть ничего, кроме оператора import Foundation.

Введите следующее:

import Foundation
public enum NetworkingError: Error {
   case unableToGenerateURL
}
public class Networking {
   public static func fetch(fromEndpoint endpoint: String, completionHandler: @escaping ((Result<Data?, Error>) -> Void)) {
      guard let url = URL(string: endpoint) else {
         let error = NetworkingError.unableToGenerateURL
         completionHandler(Result.failure(error))
         return
      }
      let session = URLSession(configuration: .default)
      let task = session.dataTask(with: url) { (data, _, error) in
         guard error == nil else {
            completionHandler(Result.failure(error!))
            return
         }
         completionHandler(Result.success((data)))
      }
      task.resume()
   }
}

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

Мы создали очень простой метод для получения данных из конечной точки. Сначала мы пытаемся сгенерировать объект URL из файла endpoint. Если это не удается, мы возвращаем ошибку. В случае успеха мы создаем URLSession с конфигурацией по умолчанию.

Затем метод URLSession dataTask сделает всю тяжелую работу по обмену данными по сети с сервером и вернет три возможных объекта: data, response и error.

Сначала мы проверим объект error, и, если это не nil, мы вернем результат ошибки с объектом error. Если error равно nil, мы вернем успешный результат с объектом data.

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

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

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

Если вы новичок в Result, просто придерживайтесь кода здесь, и к концу этой статьи вы должны его понять. Бонус!

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

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

Создайте еще один новый файл в папке источников и назовите его UserAPI.swift. Введите в новый файл следующий код:

import Foundation
public struct UserAPI {
   static let endpoint = “https://jsonplaceholder.typicode.com/users”
}

Поскольку нашему API не нужны накладные расходы класса, мы сделаем его struct. Это также логичное место для хранения строки конечной точки.

После свойства endpoint введите следующее:

public static func getUsers() {
   Networking.fetch(fromEndpoint: endpoint) { (result) in
      switch result {
      case .success(let data):
         print(data)
      case .failure(let error):
         print(error.localizedDescription)
      }
   }
}

Что здесь происходит, так это то, что у нас есть getUsers() метод, который будет вызывать Networking.fetch метод, который мы создали ранее. Он передаст свойство endpoint и простой обработчик завершения для обработки ответа result метода выборки.

На данный момент мы просто распечатаем данные, если получим успешный ответ (пока игнорируем предупреждение о неявном принуждении Xcode), или ошибку, если мы получим ответ об ошибке.

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

В навигаторе проекта щелкните файл игровой площадки JSON. Удалите весь код по умолчанию и замените его следующим:

UserAPI.getUsers()

Выполните код игровой площадки, и в журнале консоли вы должны увидеть что-то похожее на следующее:

Optional(5645 bytes)

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

Теперь, когда у нас есть данные, нам нужно что-то с ними сделать. Для этого потребуется модель данных и декодер JSON.

Модель данных

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

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

Codable на самом деле является протоколом, состоящим из протоколов Encodable и Decodable, поэтому, если вам нужно только декодировать, вы должны использовать Decodable. Мы рассмотрим циклический переход от JSON к нашей модели данных и обратно к JSON, поэтому мы используем Codable.

Сохраняя нашу тему организации кода, щелкните папку с исходными кодами в навигаторе проекта и создайте новый файл с именем DataModels.swift.

В файле замените код по умолчанию следующим:

public struct User: Codable {
   public var id: Int
   public var name: String?
   public var username: String?
   public var email: String?
   public var phone: String?
   public var website: String?
   public var address: Address?
   public var company: Company?
}
public struct Address: Codable {
   public var street: String?
   public var suite: String?
   public var city: String?
   public var zipcode: String?
   public var geo: Geo?
}
public struct Geo: Codable {
   public var lat: String?
   public var lng: String?
}
public struct Company: Codable {
   public var name: String?
   public var catchPhrase: String?
   public var bs: String?
}

Здесь мы создали набор структур, отражающих пользовательские данные JSON, возвращаемые сервером. Для сравнения давайте снова посмотрим на структуру данных JSON:

{
   id
   name
   username
   email
   phone
   website
   address {
      street
      suite
      city
      zipcode
      geo {
         lat
         lng
      }
   },
   company {
      name
      catchPhrase
      bs
   }
}

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

Каждый дискретный объект - user, address, geo и company - получает свою собственную структуру. Все значения для наших данных JSON являются строками, за исключением User.id, которое является целым числом.

Эти типы соответствуют типам, указанным в данных JSON. User.id - также единственное значение, которое, как мы знаем наверняка, вернет сервер. Все остальные значения являются необязательными, поскольку мы не можем гарантировать, что данные, которые мы получаем с сервера для каждого пользователя, будут содержать каждое из этих значений данных.

Хотя личный матч может иметь смысл большую часть времени, это не всегда необходимо. Например, в нашем случае нам не понадобится параметр bs объекта company.

Справиться с этим с помощью протокола Codable довольно просто; просто не включайте это. Удалите строку public var bs: String? из структуры Company. Это так просто.

Расшифровка

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

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

А теперь давайте создадим декодер.

Еще раз создайте новый файл в папке источников с именем JSONConverter.swift. Замените код по умолчанию в файле следующим:

import Foundation
public struct JSONConverter {
   public static func decode<T: Decodable>(_ data: Data) throws -> [T]? {
   do {
         let decoded = try JSONDecoder().decode([T].self, from: data)
         return decoded
      } catch {
         throw error
      }
   }
}

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

Давайте изучим его построчно, потому что понимание этого очень важно.

public static func decode<T: Decodable>(_ data: Data) throws -> [T]? {

При вызове метода вы передадите ему объект Data, в частности данные, которые мы получаем от функции getUsers(), и получите обратно необязательный общий массив объектов, соответствующих Decodable.

Если во время процесса возникнет ошибка, метод выдаст ошибку.

do {
   let decoded = try JSONDecoder().decode([T].self, from: data)
   return decoded
} catch {
   throw error 
}

В этом блоке do/catch происходит фактическое декодирование.

Мы вызываем метод decode JSONDecorder и передаем общий объект, соответствующий Decodable, а также наш объект data.

Другими словами, мы говорим декодеру взять наш data объект с сервера и декодировать его в массив из Decodable объектов. После декодирования мы возвращаем массив. Если метод декодирования выдает ошибку, мы ее поймаем и выбросим.

Собираем вместе

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

Откройте файл UserAPI.swift. В методе getUsers() найдите вызов Networking.fetch и замените код case .success(let data): следующим:

case .success(let data):
   guard let data = data else {
      return
   }
   do {
      let users: [User]? = try JSONConverter.decode(data)
      print(users)
   } catch {
      print(“Decoding error: \(error.localizedDescription)”)
   }

Во-первых, мы проверяем, что объект data, возвращаемый сетевым вызовом, не является nil, и, если это так, просто возвращаемся (мы сделаем что-то еще с этим через мгновение).

guard let data = data else {
   return
}

Затем мы вводим блок do/catch, чтобы отловить любые ошибки, выдаваемые декодером. Затем мы декодируем наши данные:

let users: [User]? = try JSONConverter.decode(data)

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

Назначив эти данные переменной с типом [Users]?, общий метод декодирования, который мы написали, будет использовать этот тип [Users]? при выполнении вызова JSONDecorder().decode(:). Если вы не знакомы с универсальными шаблонами, именно так декодер узнает, в какой тип объекта декодировать JSON.

В декодере писали строчку JSONDecoder().decode([T].self, from: data). Общий массив [T] в этом вызове заменяется типом [User]? во время выполнения. Все это работает, потому что Users соответствует Codable. В противном случае этот код не будет компилироваться.

   print(users)
} catch {
   print(“Decoding error: \(error.localizedDescription)”)
}

Если процесс декодирования не вызывает никаких ошибок, мы распечатываем пользовательский объект (мы устраним предупреждение, которое Xcode выдает позже). Если будет ошибка, мы ее поймаем и распечатаем.

Теперь вернитесь к файлу игровой площадки JSON и снова выполните код. Вы должны увидеть очень неаккуратный список данных, похожий на:

Optional([JSON_Sources.User(id: 1, name: Optional(“Leanne Graham”), username: Optional(“Bret”), email: Optional(“[email protected]”), phone: Optional(“1–770–736–8031 x56442”), website: Optional(“hildegard.org”), address: Optional(JSON_Sources.Address(street: Optional(“Kulas Light”), suite: Optional(“Apt. 556”), city: Optional(“Gwenborough”), zipcode: Optional(“92998–3874”), geo: Optional(JSON_Sources.Geo(lat: Optional(“-37.3159”), lng: Optional(“81.1496”))))), company: Optional(JSON_Sources.Company(name: Optional(“Romaguera-Crona”), catchPhrase: Optional(“Multi-layered client-server neural-net”))))

Поздравляем, потому что вы только что преобразовали данные сервера JSON в нашу внутреннюю модель данных!

Уборка

Теперь нам нужно немного поработать.

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

Откройте файл UserAPI.swift и измените подпись метода getUsers(), чтобы она соответствовала этому:

public static func getUsers(completionHandler: @escaping (Result<[User]?, Error>) -> Void) {

Теперь getUsers(completionHandler:) будет возвращать результат успеха / неудачи вместе с данными пользователя или данными об ошибке в обработчик завершения, который будет назначен вызывающим кодом.

Теперь замените блок кода guard let data = data следующим:

guard let data = data else {
   completionHandler(.success(nil))
   return
}

Если данные не возвращаются, мы вернем тип успеха (только потому, что сервер не отправил обратно данные, вызов все еще был успешным) с nil данными. Затем замените строку print(users) на:

completionHandler(.success(users))

И замените print(error.localizedDescription) на:

completionHandler(.failure(error))

Ваш getUsers(completionHandler:) теперь должен выглядеть так:

public static func getUsers(completionHandler: @escaping (Result<[User]?, Error>) -> Void) {
   Networking.fetch(fromEndpoint: endpoint) { (result) in
      switch result {
      case .success(let data):
         guard let data = data else {
            completionHandler(.success(nil))
            return
         }
         do {
            let users: [User]? = try JSONConverter.decode(data)
            completionHandler(.success(users))
         } catch {
            print(“Decoding error: \(error.localizedDescription)”)
         }
      case .failure(let error):
         completionHandler(.failure(error))
      }
   }
}

Вернувшись в свой JSON-файл игровой площадки, добавьте следующий метод над вызовом UserAPI.getUsers():

func printUser(_ user: User) {
   var msg = “\(user.name ?? user.id.description)”
   if let latitude = user.address?.geo?.lat,
      let longitude = user.address?.geo?.lng {
      msg += “ at coordinates \(latitude), \(longitude)”
   }
   print(msg)
}

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

Я специально решил распечатать данные широты и долготы, потому что они скрыты глубоко в данных JSON. Посмотрите, насколько легко получить доступ к этим данным теперь, когда они были декодированы в нашу модель данных.

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

Чтобы узнать имя пользователя, мы вызываем user.name. Имеет смысл. Чтобы получить данные о широте, мы вызываем user.address?.geo?.lat. Теперь мы можем просматривать многоиерархические данные JSON, которые сервер отправил нам, с простой и понятной точечной нотацией.

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

Наконец, давайте заменим вызов UserAPI.getUsers() следующим:

UserAPI.getUsers { (result) in
   switch result {
   case .success(let users):
      guard let users = users else {
         print(“No users were returned.”)
         return
      }
      users.forEach({ (user) in
         printUser(user)
      })
   case .failure(let error):
      print(error.localizedDescription)
   }
}

В нашем новом вызове UserAPI.getUsers(completionHandler:) блок завершения примет возвращенный тип результата и обработает его.

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

Выполнив код, вы должны получить следующее:

Leanne Graham at coordinates -37.3159, 81.1496
Ervin Howell at coordinates -43.9509, -34.4618
Clementine Bauch at coordinates -68.6102, -47.0653
Patricia Lebsack at coordinates 29.4572, -164.2990
Chelsey Dietrich at coordinates -31.8129, 62.5342
Mrs. Dennis Schulist at coordinates -71.4197, 71.7478
Kurtis Weissnat at coordinates 24.8918, 21.8984
Nicholas Runolfsdottir V at coordinates -14.3990, -120.7677
Glenna Reichert at coordinates 24.6463, -168.8889
Clementina DuBuque at coordinates -38.2386, 57.2232

Хорошо выглядеть!

Соответствие вашей модели данных

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

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

Если мы посмотрим на модель данных, у нас есть структура с именем Geo, которая имеет значения lat и lng, которые точно соответствуют данным JSON с сервера. Хотя теперь мы понимаем, что они означают, улучшая их, мы ничего не теряем и обретаем ясность.

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

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

Мы заменим входящие ключи lat и lng на внутренние ключи latitude и longitude для удобства чтения. Измените структуру Geo, чтобы она соответствовала следующему:

public struct Geo: Codable {
   public var latitude: String?
   public var longitude: String?
   enum CodingKeys: String, CodingKey {
      case latitude = “lat”
      case longitude = “lng”
   }
}

Здесь мы устанавливаем свойства Geo так, чтобы они соответствовали тому, что мы хотим внутри, в данном случае более читабельным latitude и longitude.

Затем мы создаем встроенное перечисление с именем CodingKeys, которое имеет тип String и соответствует CodingKey. Мы устанавливаем оператор case для каждого свойства Geo и присваиваем значения соответствующего ключа из данных JSON.

Итак, когда данные JSON для Geo содержат {“lat”: “123”, “lng”: “456”}, декодер будет использовать значения из перечисления CodingKeys для сопоставления lat с latitude и lng с longitude.

Хорошо, отлично, но как насчет сценариев, в которых мы хотим только сопоставить некоторые ключи JSON с новыми внутренними ключами?

Что ж, давайте сделаем это сейчас, изменив свойство geo из структуры Address и переименовав его в более читаемое coordinates.

Сначала переименуйте структуру Geo в Coordinates:

public struct Coordinates: Codable {

Затем в структуре Address переименуйте свойство geo в coordinates:

public var coordinates: Coordinates?

Теперь добавьте CodingKeys перечисление в структуру Address:

enum CodingKeys: String, CodingKey {
   case coordinates = “geo”
   
   case street
   case suite
   case city
   case zipcode
}

Как видите, мы предоставили только сопоставление координат нашей внутренней модели с ключом geo данных JSON. Остальные ключи остаются неизменными, и поэтому им не присвоена никакая строка.

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

Ниже представлены новые полные структуры Address и Coordinates:

public struct Address: Codable {
    public var street: String?
    public var suite: String?
    public var city: String?
    public var zipcode: String?
    public var coordinates: Coordinates?
    
    enum CodingKeys: String, CodingKey {
        case coordinates = "geo"
        
        case street
        case suite
        case city
        case zipcode
    }
}
public struct Coordinates: Codable {
    public var latitude: String?
    public var longitude: String?
    
    enum CodingKeys: String, CodingKey {
        case latitude = "lat"
        case longitude = "lng"
    }
}

Наконец, вернувшись на площадку для JSON, измените оператор if в функции printUser(:) на следующее:

if let latitude = user.address?.coordinates?.latitude,
   let longitude = user.address?.coordinates?.longitude {
   msg += “ at coordinates \(latitude), \(longitude)”
}

Это гораздо удобнее!

Изменение данных

Мы узнали, как использовать данные JSON, но теперь давайте посмотрим, как изменить эти данные.

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

Добавьте следующий код в вызов UserAPI.getUsers(completionHandler:) на игровой площадке JSON после цикла users.forEach:

var modifiedUser = users[0]
modifiedUser.company?.name = “Apple Inc.”
modifiedUser.company?.catchPhrase = “We make iThings”
modifiedUser.address?.street = “1 Infinite Loop”
modifiedUser.address?.suite = nil
modifiedUser.address?.city = “Cupertino”
modifiedUser.address?.zipcode = “95014”
modifiedUser.address?.coordinates?.latitude = “37.331586”
modifiedUser.address?.coordinates?.longitude = “-122.029895”
modifiedUser.website = “apple.com”
printUser(modifiedUser)

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

Вы должны увидеть, что у первого пользователя были координаты «-37.3159, 81.1496» при первой печати, но во второй раз наши новые координаты «37.331586, -122.029895».

Leanne Graham at coordinates -37.3159, 81.1496
…
Leanne Graham at coordinates 37.331586, -122.029895

Что ж, это было легко.

Теперь нам нужно отправить этого обновленного пользователя обратно на сервер.

Кодировка

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

Откройте файл JSONConverter.swift и после функции decode(:) добавьте следующее:

public static func encode<T: Encodable>(_ value: T) throws -> Data? {
   do {
      let data = try JSONEncoder().encode(value)
      return data
   } catch {
      throw error
   }
}

Здесь мы берем значение, которое хотим закодировать, и пропускаем его через JSONEncoder.

Поскольку значение, которое мы отправляем, - это User, которое соответствует Codable, и, по расширению Encodable, JSONEncoder знает, как кодировать нашу модель данных в данные JSON. Затем данные возвращаются в вызывающую функцию.

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

После функции getUsers(completionHandler:) добавьте следующее:

public static func saveUser(_ user: User) {
   do {
      guard let data = try JSONConverter.encode(user) else {
         return
      }
      let serialized = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
      print(serialized?.description ?? “<no serialized description>”)
   } catch {
     print(“Encoding error: \(error.localizedDescription)”)
   }
}

Сначала мы вызываем только что созданную функцию encode и сохраняем результат в переменной с именем data.

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

В файле JSON PlayStation 4 внутри блока завершения UserAPI.getUsers(completionHandler:) после строки printUser(modifiedUser) добавьте вызов для сохранения измененного пользователя:

UserAPI.saveUser(modifiedUser)

Запустите игровую площадку, и вы должны увидеть ваши измененные пользовательские данные, закодированные в формат JSON, подобный следующему:

["id": 1,
 "username": Bret,
 "name": Leanne Graham,
 "phone": 1-770-736-8031 x56442,
 "address": {
   city = Cupertino;
   geo = {
      lat = "37.331586";
      lng = "-122.029895";
   };
   street = "1 Infinite Loop";
   zipcode = 95014;
  },
 "email": [email protected], 
 "website": apple.com, 
 "company": {
   catchPhrase = "We make iThings";
   name = "Apple Inc.";
 }
]

Публикация

Мы так близки к завершению! Все, что нам нужно сделать сейчас, это отправить наши данные в кодировке JSON обратно на сервер.

Откройте файл Networking.swift и добавьте следующий код после функции fetch(fromEndpoint:completionHandler:):

public static func post(toEndpoint endpoint: String, data: Data, completionHandler: @escaping ((Result<Int, Error>) -> Void)) {
   guard let url = URL(string: endpoint) else {
      print(“Unable to generate url”)
      return
   }
   let session = URLSession(configuration: .default)
   var request = URLRequest(url: url)
   request.httpMethod = “PUT”
   request.httpBody = data
   request.setValue(“application/json”, forHTTPHeaderField: “Content-Type”)
   let task = session.dataTask(with: request) { (data, response, error) in
      guard error == nil else {
         completionHandler(Result.failure(error!))
         return
      }
      completionHandler(Result.success((response as!    HTTPURLResponse).statusCode))
   }
   task.resume()
}

Как и в случае с нашим fetch(fromEndpoint:completionHandler:) методом, понимание внутренней работы URLSession выходит за рамки этой статьи, но мы быстро рассмотрим, что здесь происходит.

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

По умолчанию httpMethod для запроса - это GET, поэтому мы изменим его на PUT. Затем мы добавляем наш объект данных в кодировке JSON в httpBody запроса и добавляем Content-Type «application / json» в HTTP-заголовок запроса.

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

Имея все это на месте, мы вызываем метод resume на task, чтобы отправить наши данные по пути.

Теперь давайте изменим метод saveUser(:) в файле UserAPI.swfift, чтобы вызвать наш новый метод post(toEndpoint:data:completionHandler:).

После print(serialized?.description ?? “<no serialized description>”) добавить:

Networking.post(toEndpoint: “\(endpoint)/\(user.id)”, data: data) { (result) in
   switch result {
   case .success(let statusCode):
      print(“Saved with code \(statusCode)”)
   case .failure(let error):
      print(“Unable to save: \(error.localizedDescription)”)
   }
}

Здесь мы вызываем наш post(toEndpoint:data:completionHandler:) метод, отправляя наш endpoint и добавляя идентификатор пользователя, чтобы сервер знал, какого пользователя мы изменяем.

Обработчик завершения просто выведет на консоль соответствующее сообщение об успешном / неудачном завершении.

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

Saved with code 200

Обратите внимание: поскольку мы используем общедоступную конечную точку JSON, сервер на самом деле не сохраняет наши данные, а просто возвращает 200.

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

Заключение

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

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

Я рекомендую вам глубже изучить протокол Codable, потому что вы можете сделать с ним больше, чем просто получать и отправлять данные JSON на сервер и с сервера.

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

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

Теперь, когда у вас есть некоторый опыт работы с Codable, избавьтесь от этих зависимостей и начните обрабатывать JSON своим собственным поддерживаемым кодом!

Вы можете найти полный исходный код игровой площадки на GitHub.