Swift HTTP JSON Communication
HTTP JSON Data Communication and Serialization in Swift
1. JSON Serialization π©βπ»
Codable
μ΄ λμ€κΈ° μ΄μ λ°©μμΌλ‘ μ§μ Serialization μ ν΄μΌνλ€.
struct CoinData: Decodable {
let time: String
let coin: String
let currency: String
let rate: Double
}
protocol CoinManagerDelegate {
func didUpdateCoin(_ manager: CoinManager, coinData: CoinData)
func didFailWithError(_ manager: CoinManager, error: Error)
}
struct CoinManager {
var delegate: CoinManagerDelegate?
func fetchData(with urlString: String) {
// 1. Create a URL
guard let url = URL(string: urlString) else { return }
// 2. Create a URLSession
var session = URLSession(configuration: .default)
/* Optional, 'value-key' pair. */
// session.setValue("apiKey", forHTTPHeaderField: "X-CoinAPI-Key")
// session.addValue("apiKey", forHTTPHeaderField: "X-CoinAPI-Key") // Same to 'setValue' method.
// 3. Give the session a task
let task: URLSessionDataTask = session.dataTask(with: url) { (data, response, error) in
if let error {
print("ERROR: \(error.localizedDescription)")
return
}
guard let data else { return }
guard let coinData = parseJson(data) else { return }
delegate?.didUpdateCoin(self, coinData: coinData)
}
// 4. Start the task
task.resume()
}
private func parseJson(_ data: Data) -> CoinData? {
do {
let coinDictionary = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
let time: String = coinDictionary?["time"] as! String
let coin: String = coinDictionary?["asset_id_base"] as! String
let currency: String = coinDictionary?["asset_id_quote"] as! String
let rate: Double = coinDictionary?["rate"] as! Double
let coinData = CoinData(time: time, coin: coin, currency: currency, rate: rate)
return coinData
} catch {
delegate?.didFailWithError(self, error: error)
return nil
}
}
}
2. Codable π©βπ»
DTO
μCodable
νλ‘ν μ½μ μ±νμν΄μΌλ‘μ¨ μΈμ½λ©κ³Ό λμ½λ©μ μμ½κ² ν μ μλ€.- Codable μ Encodable κ³Ό Decodable μ λͺ¨λ μ±ννλ alias μ΄λ€.
URLSession
μ μ¬μ©ν΄ λ°μ΄ν° ν΅μ μ μ§μ μ½λ©νκ±°λ, Codable
κ°μ²΄λ₯Ό μ§μνλ λΌμ΄λΈλ¬λ¦¬μμ μ¬μ© κ°λ₯νλ€.
struct CoinData: Decodable {
let time: String
let coin: String
let currency: String
let rate: Double
enum CodingKeys: String, CodingKey {
case time
case coin = "asset_id_base"
case currency = "asset_id_quote"
case rate
}
}
protocol CoinManagerDelegate {
func didUpdateCoin(_ manager: CoinManager, coinData: CoinData)
func didFailWithError(_ manager: CoinManager, error: Error)
}
struct CoinManager {
var delegate: CoinManagerDelegate?
func fetchData(with urlString: String) {
// 1. Create a URL
guard let url = URL(string: urlString) else { return }
// 2. Create a URLSession & task & start
/*
컀μ€ν°λ§μ΄μ§μ νμλ‘ νμ§ μλλ€λ©΄ μλμ κ°μ΄
`URLSession.shared.dataTask(with:completionHandler:)`λ₯Ό μ¬μ©ν μ μλ€.
*/
URLSession.shared.dataTask(with: url) { data, response, error in
if let error {
print("ERROR: \(error.localizedDescription)")
return
}
guard let data else { return }
guard let coinData = parseJson(data) else { return }
delegate?.didUpdateCoin(self, coinData: coinData)
}.resume()
}
private func parseJson(_ data: Data) -> CoinData? {
let decoder = JSONDecoder()
do {
let coinData = try decoder.decode(CoinData.self, from: data)
return coinData
} catch {
delegate?.didFailWithError(self, error: error)
return nil
}
}
}
μ°Έκ³ λ‘ μ μ½λλ Decodable νλ‘ν μ½μ μ±νν
CoinData DTO
λ₯Ό κ·Έλλ‘Entity
λ‘ μ¬μ©νκ³ μλ€. λ§μ½,DTO
μEntity
λ₯Ό λλμ΄ μ¬μ©ν κ²½μ°, Codable μDTO
λ§ μ±ννλ©΄ λλ€.
3. Libraries that does not support Codable π©βπ»
Firebase μ Realtime Database
, Firestore
μ κ°μ λΌμ΄λΈλ¬λ¦¬λ Codable μ μ§μνμ§ μκΈ° λλ¬Έμ
μ§μ Serialization μ ν΄μ€μΌνλ€. νμ§λ§ μ JSONSerialization μμμ κ°μ΄
Swift Data κ°μ²΄λ‘ λ°κΎΈκΈ° μν΄ μΌμΌν μ½λ©μ νλ κ²μ λΆνμν boilerplate code λ₯Ό μμ±νλ€.
λ°λΌμ, JSONSerialization
μ JSONEncoder
, JSONDecoder
κ³Ό ν¨κ» μ¬μ©νλ©΄ Codable μ μ§μνμ§ μλ
λΌμ΄λΈλ¬λ¦¬μλ μ μ©ν μ μλ€.
1. JSON Serialization Methods
jsonObject(with:options:)
: Returns a Foundation object from given JSON data.data(withJSONObject:options:)
: Returns JSON data from a Foundation object.
μ¬κΈ°μ Foundation object
λ NSArray, NSDictionary, NSNumber, NSDate, NSString, NSNull μ
μλ―Ένλ€. κ·Έλ¦¬κ³ λ°μ΄ν° ν΅μ μ ν λ κ°μ₯ λ°κΉ₯ 컨ν
μ΄λλ νμ Key-Value ννλ₯Ό νκ³ μμΌλ―λ‘, κ°μ₯ λ°κΉ₯ ννλ
[NSString: id]
νμ
μ NSDictionay λΌ λ³Ό μ μλ€. κ·Έλ¦¬κ³ Objective-C Foundation μ NSDictionay λ
Swift μ Dictionayμ λΈλ¦Ώμ§λλ―λ‘, [String: Any]
νμ
μ
Dictionary μ JSON μ¬μ΄μ νλ³νμ΄λΌ λ³Ό μ μλ€.
λ°λΌμ λ€μ μ 리νλ©΄ λ€μκ³Ό κ°λ€.
jsonObject(with:options:)
:JSON
->Dictionay
data(withJSONObject:options:)
:Dictionary
->JSON
2. JSON Encoder and Decoder Methods
JSONEncoder
Encodable
νλ‘ν μ½μ μ€μνλ Structure
λ₯Ό JSON
μΌλ‘ λ³ννλ€.
JSONDecoder
JSON
μ Decodable
νλ‘ν μ½μ μ€μνλ Structure
λ‘ λ³ννλ€.
μ΄λ₯Ό μ 리νλ©΄ λ€μκ³Ό κ°λ€.
JSONSerialization.jsonObject(with:options:)
:JSON
->Dictionary
JSONSerialization.data(withJSONObject:options:)
:Dictionary
->JSON
JSONEncoder().encode(_:)
:Structure: Encodable
->JSON
JSONDecoder().decode(_:from:)
:JSON
->Structure: Decodable
3. Casting between Dictionaries, JSON, and Structures
Codable μ μ§μνμ§ μλ Firebase μ Realtime Database, Firestore λΌμ΄λΈλ¬λ¦¬μ κ²½μ° μ§μ
JSONSerialization
λ₯Ό ν΄μΌνλ€. μ΄λ€μ [String: Any]
νμ
μ Dictionay
λ₯Ό μ¬μ©ν΄ ν΅μ νλλ°, 쿼리 κ²°κ³Ό
Response μ λ°μ΄ν°λ Any
λλ [String: Any]
νμ
μ κ°λλ€. λ°λΌμ μ§μ μ μΌλ‘ Codable μ μ¬μ©ν μ μκΈ°
λλ¬Έμ λ€μκ³Ό κ°μ λ³ν κ³Όμ μ μκ°ν΄λ³Ό μ μλ€.
Encoding
Structure
-> JSON
-> NSDictionary
Decoding
Dictionay
-> JSON
-> Structure
(or Any
-> NSDictionay
-> JSON
-> Structure
)
μ λ³ν κ³Όμ μ νμν λ©μλλ λ€μκ³Ό κ°λ€.
Encoding
Structure
-> JSON
: JSONEncoder().encode(_:)
JSON
-> NSDictionary
: JSONSerialization.jsonObject(with:options:)
Decoding
Dictionay
-> JSON
: JSONSerialization.data(withJSONObject:options:)
JSON
-> Structure
: JSONDecoder().decode(_:from:)
4. Custom Encoding Methods for Firebase
μ±μ κ°λ°νλλ° μμ΄ λ§€λ² μΈμ½λ©/λμ½λ© μ½λλ₯Ό μμ±νλ κ²μ λ§€μ° λΉν¨μ¨μ μ΄λ€. ν΄λΉ μ±μ μ λ§λλ‘ λ³λμ μ νΈμ λ§λ€μ΄ μ¬μ©νλ κ²μ΄ μ’λ€.
func encode<T>(_ value: T) throws -> Data where T : Encodable
class func jsonObject(
with data: Data,
options opt: JSONSerialization.ReadingOptions = []
) throws -> Any
λ©μλλ₯Ό μ¬μ©νλ―λ‘ T: Encodable
μ λ°μ Any
λ₯Ό λ°ννλ©΄ λλ€. μ΄λ ν¨μλ λ€μν λ°©μμΌλ‘ λ§λ€ μ μλ€.
μ€μ μ½λλ₯Ό μ¬μ©ν λ λ³λμ μλ¬ μ²λ¦¬λ₯Ό μ§μ νκΈ°λ₯Ό μνμ§ μλλ€λ©΄ μλμ κ°μ΄ μλ¬λ₯Ό throws
νμ§ μκ³ μ μ ν λ‘κ·Έλ₯Ό λ¨κΈ°κ±°λ
νΉμ μλ¦Όμ°½μ λμ°λ λ±μ 곡ν΅λ λ‘μ§μ μΆκ°νκ³ Optional
μ λ°ννλλ‘ μμ±ν μ μλ€.
func toJSON<T: Encodable>(_ data: T?) -> Data? {
guard let data else { return nil }
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
return try encoder.encode(data)
} catch let error as EncodingError {
// Common error handling here...
return nil
} catch {
// Common error handling here...
return nil
}
}
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")
let jsonData = try? toJSON(pear)
if let jsonData, let jsonData = String(data: jsonData, encoding: .utf8) {
print(jsonData)
}
λλ λ€μκ³Ό κ°μ΄ μ¬μ©νλ κ³³μμ do-catch
μ try
λ₯Ό μ¬μ©ν΄ μλ¬μ λν΄ μ§μ μ²λ¦¬νκ±°λ, try?
λ₯Ό μ¬μ©ν΄ μλ¬λ₯Ό 무μνκ³
μ€λ¨ν μ§λ₯Ό μ§μ μ ννλλ‘ ν¨μλ₯Ό μμ±ν μλ μλ€(Converting Errors to Optional Values λ₯Ό μ°Έκ³ ).
enum CastingError: Error {
case inputIsNil
}
func toJSON<T: Encodable>(_ data: T?) throws -> Data {
guard let data else { CastingError.inputIsNil }
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
return try encoder.encode(data)
} catch {
throw error
}
}
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")
// μλ¬λ₯Ό μ§μ μ²λ¦¬
do {
let jsonData = try toJSON(pear)
print(String(data: jsonData, encoding: .utf8)!)
} catch {
print(error)
}
// try? λ‘ μλ¬λ₯Ό Optional μ²λ¦¬
let jsonData = try? toJSON(pear)
if let jsonData, let jsonData = String(data: jsonData, encoding: .utf8) {
print(jsonData)
}
μ¬κΈ°μλ λ λ²μ§Έ λ°©λ²κ³Ό κ°μ΄ Input Parameters λ Optional
μ΄κ³ , throws
λ₯Ό λμ§ μ μμΌλ©°, Return Types λ
Non-Optional
μ΄ λλλ‘ μ νΈ ν¨μλ₯Ό μμ±ν κ²μ΄λ€.
enum CastingError: Error {
case inputIsNil
}
func toJSON<T: Encodable>(_ data: T?) throws -> Data {
guard let data else { throw CastingError.inputIsNil }
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try encoder.encode(data)
}
func toDictionary<T: Encodable>(_ data: T?) throws -> Any {
guard let data else { throw CastingError.inputIsNil }
let jsonData = try toJSON(data)
return try JSONSerialization.jsonObject(with: jsonData, options: .fragmentsAllowed)
}
5. Custom Decoding Methods for Firebase
class func data(
withJSONObject obj: Any,
options opt: JSONSerialization.WritingOptions = []
) throws -> Data
func decode<T>(
_ type: T.Type,
from data: Data
) throws -> T where T : Decodable
λ©μλλ₯Ό μ¬μ©νλ―λ‘ Any
λ₯Ό λ°μ T: Decodable
μ λ°ννλ©΄ λλ€.
enum CastingError: Error {
case inputIsNil
}
func fromJSON<T: Decodable>(_ type: T.Type, from data: Data?) throws -> T {
guard let data else { throw CastingError.inputIsNil }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(type, from: data)
}
func fromDictionary<T: Decodable>(_ type: T.Type, withJSONObject obj: Any?) throws -> T {
guard let obj else { throw CastingError.inputIsNil }
let data = try JSONSerialization.data(withJSONObject: obj, options: .fragmentsAllowed)
return try fromJSON(type, from: data)
}
6. Examples
struct DataUtil {
enum CastingError: Error {
case inputIsNil
}
func toJSON<T: Encodable>(_ data: T?) throws -> Data {
guard let data else { throw CastingError.inputIsNil }
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try encoder.encode(data)
}
func toDictionary<T: Encodable>(_ data: T?) throws -> Any {
guard let data else { throw CastingError.inputIsNil }
let jsonData = try toJSON(data)
return try JSONSerialization.jsonObject(with: jsonData, options: .fragmentsAllowed)
}
func fromJSON<T: Decodable>(_ type: T.Type, from data: Data?) throws -> T {
guard let data else { throw CastingError.inputIsNil }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(type, from: data)
}
func fromDictionary<T: Decodable>(_ type: T.Type, withJSONObject obj: Any?) throws -> T {
guard let obj else { throw CastingError.inputIsNil }
let data = try JSONSerialization.data(withJSONObject: obj, options: .fragmentsAllowed)
return try fromJSON(type, from: data)
}
func toUserDefaults<T: Encodable>(_ value: T, forkey: String) throws {
let encodedValue = try PropertyListEncoder().encode(value)
UserDefaults.standard.setValue(encodedValue, forKey: forkey)
}
func fromUserDefaults<T: Decodable>(_ type: T.Type, forkey: String) throws -> T? {
guard let data = UserDefaults.standard.object(forKey: forkey) as? Data else { return nil }
return try PropertyListDecoder().decode(type, from: data)
}
}
class CardListTableViewController: UITableViewController {
// Firestore
var db: Firestore!
var creditCards: [CreditCard] = []
override func viewDidLoad() {
super.viewDidLoad()
// Database
db = Firestore.firestore()
// MARK: Firebase Firestore GET
db.collection("creditCardList").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("ERROR: Fetching documents \(error!.localizedDescription)")
return
}
self.creditCards = documents.compactMap {
let document: [String: Any] = $0.data()
guard let cardData = try? DataUtil().fromDictionary(CreditCard.self, withJSONObject: document) else { return nil }
return cardData
}.sorted { $0.rank < $1.rank }
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
private func uploadCard(cardData: CreditCard) {
guard let cardData = try? DataUtil().toDictionary(cardData) else { return }
let encoder = JSONEncoder()
guard let data = try? encoder.encode(data) else { return }
guard let data = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
db.collection("creditCardList").document("Item1").setData(data)
}
}
μ΄λ°μμΌλ‘ λ°λ³΅λλ Decoding μ λ¬Όλ‘ , Encoding μ²λ¦¬ μμ μ λΆλ¦¬μν¬ μ μλ€.
4. CodingKeys vs. Computed π©βπ»
API ν΅μ μ ν λ μ±μμ μ¬μ©νλ κ°μ²΄μ APIκ° μ¬μ©νλ κ°μ²΄κ° λμΌνλ€λ 보μ₯μ΄ μλ€. κ·Έλ λ€κ³ API ν΅μ μ λ°κ±°λ λ³΄λΌ λ λ§€λ² κ°μ²΄λ₯Ό Converting νλ κ²μ λ§€μ° μλͺ¨μ μΈ μΌμ΄λ€. μ΄λ¬ν λ¬Έμ λ₯Ό Swift μμλ λ κ°μ§ λ°©λ²μΌλ‘ ν΄κ²°ν μ μλ€.
1. CodingKeys
struct CoinData: Codable {
let time: String
let coin: String
let currency: String
let rate: Double
enum CodingKeys: String, CodingKey {
case time
case coin = "asset_id_base"
case currency = "asset_id_quote"
case rate
}
}
κ°μ₯ 보νΈμ μΈ λ°©λ²μΌλ‘ Swift κ° μ 곡νλ CodingKeys
Enumerations λ₯Ό μ¬μ©νλ κ²μ΄λ€. μμ κ°μ΄ μμ±νλ©΄
μΈμ½λ©, λμ½λ© μ μλμΌλ‘ Key κ° λ§€μΉλλ€.
2. Computed
struct CoinData: Codable {
let time: String
private var asset_id_base: String
var coin: String {
get { asset_id_base }
set { asset_id_base = newValue }
}
private var asset_id_quote: String
var currency: String {
get { asset_id_quote }
set { asset_id_quote = newValue }
}
let rate: Double
init(time: String, coin: String, currency: String, rate: Double) {
self.time = time
self.asset_id_base = coin
self.asset_id_quote = currency
self.rate = rate
}
}
λ λ€λ₯Έ λ°©λ²μΌλ‘λ Computed Properties
λ₯Ό μ¬μ©νλ κ²μ΄λ€. λ¨, μ΄ κ²½μ° Computed Properties λ₯Ό μ¬μ©νκΈ°
λλ¬Έμ Memberwise Initializers κ° μμ±λμ§ μκΈ° λλ¬Έμ
μ§μ Initializers λ₯Ό λ§λ€μ΄μΌνλ€.
μ CodingKeys μ Computed Properties λ λ€μ μ½λλ₯Ό ν΅ν΄ ν μ€νΈ κ°λ₯νλ€.
// Create a CoinData structure
let coinData = CoinData(time: "2024-01-23T12:34:56Z",
coin: "BTC",
currency: "USD",
rate: 40000.50)
// 1. toJSON(_:) test
do {
let jsonData = try DataUtil().toJSON(coinData)
let jsonString = String(data: jsonData, encoding: .utf8)
print("CoinData as JSON:\n\(jsonString ?? "Unable to convert to JSON")")
} catch {
print("Error converting CoinData to JSON: \(error)")
}
// 2. toDictionary(_:) test
do {
let dataUtil = DataUtil()
let jsonDictionary = try dataUtil.toDictionary(coinData)
print("CoinData as Dictionary:\n\(jsonDictionary)")
} catch {
print("Error converting CoinData to Dictionary: \(error)")
}
// Create a JSON String
let jsonData = """
{
"time": "2024-01-23T12:34:56Z",
"asset_id_base": "BTC",
"asset_id_quote": "USD",
"rate": 40000.50
}
""".data(using: .utf8)
// 3. fromJSON(_:from:) test
do {
let coinDataFromJSON = try DataUtil().fromJSON(CoinData.self, from: jsonData)
print("CoinData from JSON:\n\(coinDataFromJSON)")
} catch {
print("Error converting JSON to CoinData: \(error)")
}
// Create a dictionary
let jsonDictionary: [String: Any] = [
"time": "2024-01-23T12:34:56Z",
"asset_id_base": "BTC",
"asset_id_quote": "USD",
"rate": 40000.50
]
// 4. fromDictionary(_:withJSONObject:) test
do {
let coinDataFromDict = try DataUtil().fromDictionary(CoinData.self, withJSONObject: jsonDictionary)
print("CoinData from Dictionary:\n\(coinDataFromDict)")
} catch {
print("Error converting Dictionary to CoinData: \(error)")
}
Reference
- βCodable.β Apple Developer Documentation. accessed Jan. 16, 2024, Apple Developer Documentation - Swift/Swift Standard Library/Codable.
- βJSONSerialization.β Apple Developer Documentation. accessed Jan. 16, 2024, Apple Developer Documentation - Foundation/Archives and Serialization/JSONSerialization.