Swift 使用JSON資料結構

黑暗森林的歌者發表於2018-02-26

如果你的APP從伺服器獲取到的資料格式為JSON。你可以使用JSONSerialization把JSON解析成Swift的資料型別,比如Dictionary,Array,String,Number,Bool。不過,因為你的APP不能直接使用JSON的結構,可以將它解析成模型物件。本文描述了一些方法可以讓你的APP使用JSON資料。

從JSON中取值

JSONSerialization中有個方法jsonObject(with:options:)可以返回一個型別為Any的資料,並且如果data資料不能被解析會返回錯誤資訊

import Foundation

let data: Data // 從網路請求下來的資料
let json = try? JSONSerialization.jsonObject(with: data, options: [])

複製程式碼

儘管JSON資料中只包含一個值,每次請求都會返回一個物件或者陣列。可以使用optional型別使用as?,在if或者guard條件語句中提取出自定義的資料。從JSON中提取Dictionary資料需要元素條件是[String: Any]。提取Array型別資料需要的元素條件是[Any](或者更加明確的陣列元素型別,比如[Strng])。可以使用字典的鍵值或者陣列的下標配合元素的型別來獲取對應的資料。

// JSON中物件
/*
	{
		"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
	}
}

// JSON中是陣列
/*
	[
		"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
	}
}
複製程式碼

Swift包含基礎API可以安全、快速的提取和處理JSON資料。

根據JSON建立模型物件

大多數APP遵循MVC設計模式,通常將JSON轉成APP中指定的模型物件

例如,編寫一個為搜尋當地參觀的APP,你可能需要建立一個接收JSON資料並初始化為餐廳模型的方法,用一個非同步網路請求,然後返回一個包含餐廳物件的陣列。

例如下面的餐廳模型:

import Foundation

struct Restaurant {
	enum Meal: String {
		case breakfast, lunch, dinner
	}

	let name: String
	let location: (latitude: Double, longitude: Double)
	let meals: Set<Meal>
}
複製程式碼

餐廳有一個String型別的名字,一個位置座標,和一個包含進餐型別列舉值

下面可能是一個伺服器返回的餐廳資料:

{
	"name": "Caffè Macs",
	"coordinates": {
		"lat": 37.330576,
		"lng": -122.029739
	},
	"meals": ["breakfast", "lunch", "dinner"]
}
複製程式碼

寫一個JSON的可選型初始值

從JSON中初始化一個餐廳物件,將JSON資料提取和轉換成一個Any型別的物件

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
	}
}
複製程式碼

如果你的APP伺服器不會只返回給一種模型物件,應該考慮實現不同的初始化方法處理每一種可能的型別。 在上面的示例中,提取到的每個常量的值是通過JSON使用可選值指定為字典。 進餐名稱提取的資料只能作為初始值。經度緯度可以組合成一個元組。進餐的型別可以使用列舉值來表示。

處理JSON初始化錯誤

上面的示例實現了一個可選型的初始化,如果失敗,則返回nil。或者你可以定義一個符合協議的型別來表示初始化錯誤,當反序列化失敗的時候丟擲一個錯誤型別。

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
	}
}
複製程式碼

在這裡,餐廳型別巢狀的宣告一個SerializationError的列舉型別,定義列舉值的屬性為相關值缺失或者相關值無效。在這一版本的JSON初始化中,不再是通過返回nil表示失敗,而是返回一個具體的錯誤原因。這一版本中還加入了對資料的驗證(確保座標值代表一個有效的地理座標,每個進餐名字對應指定的列舉值)。

獲取結果的方法

伺服器通常在一個JSON中返回多個結果。例如,搜尋服務可能返回包含0個或者多個餐廳及匹配的請求引數,還包括其他的一些資料:

{
	"query": "sandwich",
	"results_count": 12,
	"page": 1,
	"results": [
		{
			"name": "Caffè Macs",
			"coordinates": {
				"lat": 37.330576,
				"lng": -122.029739
			},
			"meals": ["breakfast", "lunch", "dinner"]
		},
		...
	]
}
複製程式碼

你可以建立一個類方法,將一個相應的餐廳物件轉換成查詢方法的引數,通過http請求傳送給伺服器。這段程式碼同時負責響應處理伺服器返回的JSON資料,非同步處理JSON中返回的陣列結果,反序列化成餐廳物件

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()
	}
}
複製程式碼

當使用者在搜尋欄輸入文字的時候,檢視控制器呼叫這個方法對餐廳進行匹配:

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()
			}
		}
	}
}
複製程式碼

即便是網路服務的細節發生變化,檢視控制器只是以用分離封裝的介面方法訪問餐廳資料,與網路服務部分降低耦合

參考蘋果的關於Swift使用JSON的官方部落格

相關文章