테이블 뷰 JSON 데이터로 가져오기


iOS앱에서 테이블뷰는 대부분 필수적으로 개발을하게 되는데, 특히나 쇼핑몰 서비스는 더욱 더 테이블 뷰가 필수적이다.

아무튼 테이블 뷰를 요구사항대로 개발하면서 많은 것을 배울 수 있었다. 특히나 JSONDecoder와 KeyDecodingStrategy를 사용하여 json파일을 굉장히 편하게 decoding할 수 있는 점이 신선한 충격이었다.

  1. Codable - Encodable & Decodable
  2. JSONDecoder
  3. KeyDecodingStrategy


1. Codable - Encodable & Decodable

Codable은 공식문서에 따르면 JSON과 같은 외부 표현형식(external representation) 호환성을 위해 data type을 encodable, decodable하게 만들기 위한 type이다.

Document

또한 Codable이 struct에도 채택될 수 있어 편하게 json 확장자를 decoding이 가능하다.

url

url로 json을 가져올 수 있지만, 요구사항이 파일로 만들어 Xcode 내에 파일을 넣어 접근하는 방식이다. 따라서 먼저 해야할 작업은 json 파일을 JSONDecoder의 인자로 사용할 수 있게 Data type으로 변환해주는 것이다.

TableView에 들어갈 data여서 TableViewModel이라는 class를 따로 만들고 UITableViewDataSource 프로토콜을 채택하여 Data 관리는 따로 해주도록 하였다.

// Bundle로 접근하여 url을 얻어왔다.
guard let url = Bundle.main.url(forResource: "main", withExtension: "json") else { return }

Document

URL

A value that identifies the location of a resource, such as an item on a remote server or the path to a local file.

URLSession.shared.dataTask

url은 location이기에 이 경로에 있는 json 파일을 data로 반환해주는 작업을 위해 dataTask 메소드를 사용한다.

        URLSession.shared.dataTask(with: url) { (data, _, err) in
            if let err = err {
                print("Failed to request data", err)
            }
                                               
            guard let data = data else { return }
            ...
        }.resume()

URLSession.shared : URLSession singleton

dataTask(with: url)

Creates a task that retrieves the contents of the specified URL, then calls a handler upon completion.

After you create the task, you must start it by calling its resume() method.

By using the completion handler, the task bypasses calls to delegate methods for response and data delivery, and instead provides any resulting NSData, URLResponse, and NSError objects inside the completion handler. Delegate methods for handling authentication challenges, however, are still called.

You should pass a nil completion handler only when creating tasks in sessions whose delegates include a urlSession(_:dataTask:didReceive:)method.

If the request completes successfully, the data parameter of the completion handler block contains the resource data, and the error parameter is nil. If the request fails, the data parameter is niland the error parameter contain information about the failure. If a response from the server is received, regardless of whether the request completes successfully or fails, the response parameter contains that information.

request가 성공적으로 마치면, data parameter는 리소스 데이터를 가지게 되고, error parameter는 nil이 되게 된다.

반대로 실패하게 되면, data 가 nil이 되고 error 는 실패 정보값을 갖게 된다. 성공여부와 관계없이 response parameter는 정보를 담고있다.


2. JSONDecoder

배열 값이 아닌 JSON Decoding

JSONDecoder 객체를 통해 decoding을 해주면 된다.

        URLSession.shared.dataTask(with: url) { (data, _, err) in
            if let err = err {
                print("Failed to request data", err)
            }
            
            guard let data = data else { return }
            
            let jsonDecoder = JSONDecoder()
            
            do {
                let storeItem = try jsonDecoder.decode(StoreItem.self, from: data)
            } catch let jsonErr {
                print("Error in Serialization", jsonErr)
            }
        }.resume()


decode

decode(_:from:)

Decodes an instance of the indicated type.

declaration

func decode<T>(_ type: T.Type, from: Self.Input) throws -> T where T : Decodable

decode 메소드의 첫번째 parameter에는 특이하게 type이 들어간다. 여기에 넣은 type을 반환하기 때문에 이곳에 struct 이름을 적어주면 바로 decoding된 struct를 반환해준다.

decode를 사용할 때 주의할 점은, JSON에 명시된 key값 명과 struct의 property 이름을 똑같이 해줘야한다. 하나라도 틀리거나 optional을 잘못 선언하면 error가 발생한다.

여기서 optional하나를 잘못선언하여서 꽤 오랫동안 막혀있었다.

StoreItem struct

struct StoreItem: Decodable {
    let detailHash, image, alt, description, sPrice, title: String
    let nPrice: String?
    
    let deliveryType, badge: [String]?
}

모든 JSON을 파싱했을 때 값이 있다면 optional ? 을 붙이지 않아도 되지만, 하나라도 nil값일 때가 있다면 꼭 optional을 지정해줘야한다.


Decoding JSON with Array

            do {
                let storeItems = try jsonDecoder.decode([StoreItem].self, from: data)
            } catch let jsonErr {
                print("Error in Serialization", jsonErr)
            }
        }.resume()

type parameter를 배열로만 바꿔주면 된다.


3. KeyDecodingStrategy

JSONDecoder를 사용하면 간편하게 JSON key값의 이름을 가진 property들로 decoding이 되지만, JSON 파일에 key값이 예쁘지 않다면 property명으로 쓰기 꺼려질 때가 있다. 그럴 때 사용하는 것이 KeyDecodingsStrategy이다.

Document

A value that determines how a type’s coding keys are decoded from JSON keys.

var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy
  • convertFromSnakeCase
  • useDefaultKeys
  • custom


convertFromSnakeCase

JSON에서는 보통 Snake Case를 사용하는 경우가 많기 때문에 이를 Camel Case로 바꿔주는 방식이다.

Document

The following examples show the result of applying this strategy:

fee_fi_fo_fum
		Converts to: feeFiFoFum
feeFiFoFum
		Converts to: feeFiFoFum
base_uri
		Converts to: baseUri


useDefaultKeys

A key decoding strategy that doesn’t change key names during decoding.


custom

A key decoding strategy defined by the closure you supply.

클로저를 사용해서 custom하게 keyCoding을 설정할 수 있다. 아래 document 예제를 참고하면 좋다.

struct A: Codable {
    var value: Int
    var b: B
    
    struct B: Codable {
        var value: Int
        var c: C
        
        struct C: Codable {
            var value: Int
        }
    }
}

let json = """
{
    "a.value": 1,
    "b": {
        "a.b.value": 2,
        "c": {
            "a.b.c.value": 3
        }
    }
}
""".data(using: .utf8)!

let decoder = JSONDecoder()

/// An implementation of CodingKey that's useful for combining and transforming keys as strings.
struct AnyKey: CodingKey {
    var stringValue: String
    var intValue: Int?
    
    init?(stringValue: String) {
        self.stringValue = stringValue
        self.intValue = nil
    }
    
    init?(intValue: Int) {
        self.stringValue = String(intValue)
        self.intValue = intValue
    }
}


// Customize each key to remove any preceding, dot-syntax paths.
decoder.keyDecodingStrategy = .custom { keys in
    let lastComponent = keys.last!.stringValue.split(separator: ".").last!
    return AnyKey(stringValue: String(lastComponent))!
}

let a = try decoder.decode(A.self, from: json)

print(a.b.c.value) // Prints "3"

왜 AnyKey를 만들어서 반환하는지 이해가 잘 안됐는데, string값인 CodingKey 값으로 넘겨주려고 하는 것 같다.


References

태그: ,

카테고리:

업데이트:

댓글남기기