Wie Sie mit JSON in Swift arbeiten.
Wenn ihre Anwendung mit einer Webanwendung kommuniziert, werden die vom Server zurückgegebenen Informationen oft als JSON formatiert. Sie können die JSONSerialization-Klasse des Foundation-Frameworks verwenden, um JSON in Swift-Datentypen wie Dictionary, Array, String, Number und Bool zu konvertieren. Da Sie sich jedoch nicht sicher sein können, ob die Struktur oder die Werte von JSON in ihrer Anwendung vorhanden sind, kann es schwierig sein, Modellobjekte korrekt zu deserialisieren. Dieser Beitrag beschreibt einige Ansätze, die Sie bei der Arbeit mit JSON in ihren Anwendungen verfolgen können.
Werte aus JSON extrahieren.
Die JSONSerialization-Klassenmethode jsonObject (with: Options) gibt einen Wert vom Typ Any zurück und gibt einen Fehler aus, wenn die Daten nicht geparst werden konnten.
import Foundation
let data: Data // received from a network request, for example
let json = try? JSONSerialization.jsonObject(with: data, options: [])
Obwohl gültiger JSON nur einen einzigen Wert enthalten kann, kodiert eine Antwort einer Web-Anwendung typischerweise ein Objekt oder Array als Top-Level-Objekt. Nutzen Sie alternativ das Binding und geben Sie den Operator cast in eine if- oder guard-Anweisung ein, um einen Wert des bekannten Typs als Konstante zu extrahieren. Um einen Dictionary-Wert von einem JSON-Objekttyp zu erhalten, casten Sie ihn bedingt als [String: Any]. Um einen Array-Wert von einem JSON-Array-Typ zu erhalten, müssen Sie ihn bedingt als [Any] (oder ein Array mit einem spezifischeren Elementtyp, wie z.B. [String]) ausgeben. Sie können einen Wörterbuchwert nach Schlüssel oder einen Arraywert nach Index extrahieren, indem Sie den Typ cast optionale Bindung mit Indexzugriffen oder Musterabgleich mit Aufzählung verwenden.
// Example JSON with object root:
/*
{
"someKey": 42.0,
"anotherKey": {
"someNestedKey": true
}
}
*/
if let dictionary = jsonWithObjectRoot as? [String: Any] {
if let number = dictionary["someKey"] as? Double {
// access individual value in dictionary
}
for (key, value) in dictionary {
// access all key / value pairs in dictionary
}
if let nestedDictionary = dictionary["anotherKey"] as? [String: Any] {
// access nested dictionary values by key
}
}
// Example JSON with array root:
/*
[
"hello", 3, true
]
*/
if let array = jsonWithArrayRoot as? [Any] {
if let firstObject = array.first {
// access individual object in array
}
for object in array {
// access all objects in array
}
for case let string as String in array {
// access only string values in array
}
}
Die integrierten Sprachfunktionen von Swift erleichtern das sichere Extrahieren und Arbeiten mit JSON-Daten, die mit Foundation-APIs dekodiert wurden – ohne die Notwendigkeit einer externen Bibliothek oder eines Frameworks.
Modellobjekte aus JSON-extrahierten Werten anlegen.
Da die meisten Swift-Anwendungen dem Model-View-Controller-Design folgen, ist es oft sinnvoll, JSON-Daten in Objekte zu konvertieren, die für die Domain ihrer App in einer Modelldefinition spezifisch sind.
Wenn Sie beispielsweise eine Anwendung schreiben, die Suchergebnisse für lokale Restaurants liefert, können Sie ein Restaurant-Modell mit einem Initializer implementieren, der ein JSON-Objekt und eine Typmethode akzeptiert, die eine HTTP-Anfrage an den Endpunkt /search eines Servers stellt und dann asynchron ein Array von Restaurant-Objekten zurückgibt.
Betrachten Sie das folgende Restaurant-Modell:
import Foundation
struct Restaurant {
enum Meal: String {
case breakfast, lunch, dinner
}
let name: String
let location: (latitude: Double, longitude: Double)
let meals: Set<Meal>
}
Ein Restaurant hat einen Namen vom Typ String, einen Ort, der als Koordinatenpaar ausgedrückt wird und einen Satz von Mahlzeiten, die Werte einer verschachtelten Mahlzeitenaufzählung enthalten.
Hier ist ein Beispiel, wie ein einzelnes Restaurant in einer Serverantwort dargestellt werden kann:
{
"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"meals": ["breakfast", "lunch", "dinner"]
}
Schreiben eines optionalen JSON-Initializers.
Um von einer JSON-Darstellung in ein Restaurant-Objekt zu konvertieren, schreiben Sie einen Initalizer, der ein Any-Argument verwendet, das Daten aus der JSON-Darstellung extrahiert und in Eigenschaften umwandelt.
extension Restaurant {
init?(json: [String: Any]) {
guard let name = json["name"] as? String,
let coordinatesJSON = json["coordinates"] as? [String: Double],
let latitude = coordinatesJSON["lat"],
let longitude = coordinatesJSON["lng"],
let mealsJSON = json["meals"] as? [String]
else {
return nil
}
var meals: Set<Meal> = []
for string in mealsJSON {
guard let meal = Meal(rawValue: string) else {
return nil
}
meals.insert(meal)
}
self.name = name
self.coordinates = (latitude, longitude)
self.meals = meals
}
}
Wenn ihre Anwendung mit einem oder mehreren Webservices kommuniziert, die nicht eine einzige, konsistente Darstellung eines Modellobjekts zurückgeben, sollten Sie in Erwägung ziehen, mehrere Initialisierungen zu implementieren, um jede der möglichen Darstellungen zu behandeln.
Im obigen Beispiel wird jeder der Werte in Konstanten aus dem übergebenen JSON-Wörterbuch mittels optionaler Bindung und dem Casting-Operator vom Typ? Extrahiert. Für die Eigenschaft name wird der extrahierte Namenswert einfach so zugewiesen, wie er ist. Für die Koordinateneigenschaft werden die extrahierten Breiten- und Längengrade vor der Zuweisung zu einem Tupel zusammengefasst. Für die Eigenschaft meals werden die extrahierten String-Werte iteriert, um einen Satz von Aufzählungswerten für Mahlzeiten zu erstellen.
Schreiben eines JSON-Initializers mit Fehlerbehandlung.
Das vorherige Beispiel implementiert einen optionalen Initializer, der null zurückgibt, wenn die Deserialisierung fehlschlägt. Alternativ können Sie einen dem Fehlerprotokoll entsprechenden Typ definieren und einen Initializer implementieren, der einen Fehler dieses Typs auslöst, wenn die Deserialisierung fehlschlägt.
enum SerializationError: Error {
case missing(String)
case invalid(String, Any)
}
extension Restaurant {
init(json: [String: Any]) throws {
// Extract name
guard let name = json["name"] as? String else {
throw SerializationError.missing("name")
}
// Extract and validate coordinates
guard let coordinatesJSON = json["coordinates"] as? [String: Double],
let latitude = coordinatesJSON["lat"],
let longitude = coordinatesJSON["lng"]
else {
throw SerializationError.missing("coordinates")
}
let coordinates = (latitude, longitude)
guard case (-90...90, -180...180) = coordinates else {
throw SerializationError.invalid("coordinates", coordinates)
}
// Extract and validate meals
guard let mealsJSON = json["meals"] as? [String] else {
throw SerializationError.missing("meals")
}
var meals: Set<Meal> = []
for string in mealsJSON {
guard let meal = Meal(rawValue: string) else {
throw SerializationError.invalid("meals", string)
}
meals.insert(meal)
}
// Initialize properties
self.name = name
self.coordinates = coordinates
self.meals = meals
}
}
Hier deklariert der Restauranttyp einen geschachtelten SerializationError-Typ, der Aufzählungsfälle mit zugehörigen Werten für fehlende oder ungültige Eigenschaften definiert. In der Wurfversion der JSON-Initialisatoren wird kein Fehler angezeigt, sondern ein Fehler geworfen, um den spezifischen Fehler zu melden. Diese Version führt auch eine Validierung der Eingabedaten durch, um sicherzustellen, dass die Koordinaten ein gültiges geographisches Koordinatenpaar darstellen und dass jeder der im JSON angegebenen Namen für Mahlzeiten den Fällen der Mahlzeitenaufzählung entspricht.
Schreiben einer Typmethode für das Holen von Ergebnissen.
Ein Web-Anwendungsendpunkt gibt oft mehrere Ressourcen in einer einzigen JSON-Antwort zurück. Ein /search-Endpunkt kann beispielsweise null oder mehr Restaurants zurückgeben, die dem angeforderten Abfrageparameter entsprechen und diese Darstellungen zusammen mit anderen Metadaten enthalten:
{
"query": "sandwich",
"results_count": 12,
"page": 1,
"results": [
{
"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"meals": ["breakfast", "lunch", "dinner"]
},
...
]
}
Sie können auf der Restaurant-Struktur eine Typmethode anlegen, die einen Query-Methodenparameter in ein entsprechendes Request-Objekt operationalisiert und den HTTP-Request an den Webservice sendet, Dieser Code wäre auch für die Behandlung der Antwort, die Deserialisierung der JSON-Daten, die Erstellung von Restaurant-Objekten aus jedem der extrahierten Wörterbücher im „results“-Array und deren asynchrone Rückgabe in einem Completion-Handler zuständig.
extension Restaurant {
private let urlComponents: URLComponents // base URL components of the web service
private let session: URLSession // shared session for interacting with the web service
static func restaurants(matching query: String, completion: ([Restaurant]) -> Void) {
var searchURLComponents = urlComponents
searchURLComponents.path = "/search"
searchURLComponents.queryItems = [URLQueryItem(name: "q", value: query)]
let searchURL = searchURLComponents.url!
session.dataTask(url: searchURL, completion: { (_, _, data, _)
var restaurants: [Restaurant] = []
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
for case let result in json["results"] {
if let restaurant = Restaurant(json: result) {
restaurants.append(restaurant)
}
}
}
completion(restaurants)
}).resume()
}
}
Ein View-Controller kann diese Methode aufrufen, wenn der Benutzer Text in eine Suchleiste eingibt, um eine Tabellenansicht mit passenden Restaurants zu füllen:
import UIKit
extension ViewController: UISearchResultsUpdating {
func updateSearchResultsForSearchController(_ searchController: UISearchController) {
if let query = searchController.searchBar.text, !query.isEmpty {
Restaurant.restaurants(matching: query) { restaurants in
self.restaurants = restaurants
self.tableView.reloadData()
}
}
}
}
Eine solche Trennung bietet eine konsistente Schnittstelle für den Zugriff auf Restaurantressourcen von View-Controllern, auch wenn sich die Implementierungsdetails über den Webservice ändern.
Reflektion über Reflexion.
Die Konvertierung zwischen Darstellungen der gleichen Daten, um zwischen verschiedenen Systemen zu kommunizieren, ist eine mühsame, wenn auch notwendige Aufgabe, um Software zu schreiben.
Da die Struktur dieser Darstellungen sehr ähnlich sein kann, ist es verlockend, eine übergeordnete Abstraktion zu erstellen, um automatisch zwischen diesen verschiedenen Darstellungen abzubilden. Beispielsweise könnte ein Typ ein Mapping zwischen snake_case JSON-Schlüsseln und cameICase-Eigenschaftsnamen definieren, um ein Modell von JSON mit Hilfe der Swift-reflection-APIs wie Mirror automatisch zu initialisieren.
Wir haben jedoch festgestellt, dass diese Art von Abstraktionen tendenziell keine signifikanten Vorteile gegenüber der herkömmlichen Verwendung von Swift-Sprachfunktionen bieten und es stattdessen schwieriger machen, Probleme zu debuggen oder Kantenfälle zu behandeln. Im obigen Beispiel extrahiert und mappt der Initializer nicht nur Werte aus JSON, sondern initialisiert auch komplexere Datentypen und führt eine domainspezifische Eingabevalidierung durch. Ein reflexionsbasierter Ansatz müsste große Anstrengungen unternehmen, um all diese Aufgaben zu erfüllen. Denken Sie daran, wenn Sie die verfügbaren Strategien für ihre eigene Anwendung evaluieren. Die Kosten für kleine Mengen der Vervielfältigung können erheblich geringer sein, als die Auswahl der falschen Abstraktionen.