Jiseob Kim

iOS Developer

Swift - Property Wrapper (feat. Codable Final)

12 Jul 2021 » Swift


์ง€๋‚œ ํŽธ์— ์ด์–ด์„œ ์ง„ํ–‰!


์ด์ œ ์ง„์งœ ๋งˆ์ง€๋ง‰ ํŒŒํŠธ๋‹ค.


์ด์ „ํŽธ์—์„œ๋Š” JSON๋ฐ์ดํ„ฐ์— ํ•ด๋‹น ํ‚ค๊ฐ€ ์—†์–ด๋„ ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง€ ์•Š๊ณ  ๊ธฐ๋ณธ๊ฐ’์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.


์ด๋•Œ, Generic์„ ์ด์šฉํ•˜์˜€์—ˆ๋‹ค.


์ด๋ฒˆ ํŽธ์€ ์•„์ฃผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋๋‚ผ ๊ฒƒ์ด๋‹ค.

๋‹ค๋ฅธ ํƒ€์ž…์œผ๋กœ ํ˜•๋ณ€ํ™˜ ํ•˜๋Š” ๊ฒƒ๊ณผ ๋ฆฌ์ŠคํŠธ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋‹ค.


๋‹ค๋ฅธ ํƒ€์ž…์œผ๋กœ ๋ฐ”๊พธ๋Š”๊ฑด ์˜ต์…”๋„์ธ ํƒ€์ž…๊ณผ ์˜ต์…”๋„์ด ์•„๋‹Œ ํƒ€์ž…์„ ์ ์šฉํ•  ๊ฒƒ์ด๋‹ค.


์˜ต์…”๋„์ด ์•„๋‹Œ ํƒ€์ž…

์˜ต์…”๋„์ด ์•„๋‹ˆ๊ณ  ๋‹ค๋ฅธ ํƒ€์ž… ์บ์ŠคํŒ…์„ ๋ณด์ž

์ด์ „์— ํ–ˆ๋˜

{
    "isHidden" : "Y"
}


์ด ๊ฐ’์„ Bool ํ˜•ํƒœ๋กœ ๋ฐ›๊ธฐ๋ฅผ ์ง„ํ–‰ ํ• ๊ฑด๋ฐ,

์ด๊ฒƒ๋„ ๊ธฐ๋ณธ๊ฐ’์ด true ๋˜๋Š” false์ด๋ฏ€๋กœ

๋‹ค์Œ ๊ฒƒ๋“ค๋„ 2๊ฐœ๊ฐ€ ๋‚˜์˜ฌ ๊ฒƒ์ด๋‹ค.

  • init(from decoder: Decoder){}
  • extension KeyedDecodingContainer{}


๊ทธ๋Ÿฌ๋ฏ€๋กœ ๊ทธ๋ƒฅ protocol๋กœ ๋ฌถ์–ด์ฃผ๊ณ 

protocol JSONStringConverterAvailable {
    static var defaultValue: Bool { get }
}


์ด์ „์— ๋งŒ๋“  JSONDefaultWrapper์•ˆ์— Property Wrapper ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๊ณ 

enum JSONDefaultWrapper {
    // ...
    
    // Property Wrapper - Optional String To Bool
    @propertyWrapper
    struct StringConverterWrapper<T: JSONStringConverterAvailable> {
        var wrappedValue: Bool = T.defaultValue
    }
    
    // ...
}


Decodable๋„ ์ ์šฉํ•ด์ฃผ๊ณ 

extension JSONDefaultWrapper.StringConverterWrapper: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.wrappedValue = (try container.decode(String.self)) == "Y"
    }
}


KeyedDecodingContainer ํ™•์žฅ๋„ ํ•ด์ฃผ๊ณ 

extension KeyedDecodingContainer {
    // ...
    
    func decode<T: JSONStringConverterAvailable>(_ type: JSONDefaultWrapper.StringConverterWrapper<T>.Type, forKey key: Key) throws -> JSONDefaultWrapper.StringConverterWrapper<T> {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}


์ด์ „ ํŽธ๋“ค์„ ๋ดค๋‹ค๋ฉด ์ดํ•ดํ•˜๊ธฐ ํ›จ์”ฌ ์ˆ˜์›”ํ•  ๊ฒƒ์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์“ฐ๊ธฐ ์œ„ํ•ด ์ด์ „ํŽธ์˜ JSONDefaultWrapper.TypeCase์—๋„ ์ •์˜๋ฅผ ํ•ด์ค€๋‹ค.


๊ธฐ๋ณธ๊ฐ’ true, false ๋‘˜๋‹ค!

enum TypeCase {
    enum StringFalse: JSONStringConverterAvailable {
        // ๊ธฐ๋ณธ๊ฐ’ - false
        static var defaultValue: Bool { false }
    }

    enum StringTrue: JSONStringConverterAvailable {
        // ๊ธฐ๋ณธ๊ฐ’ - false
        static var defaultValue: Bool { true }
    }
}


๊ทธ๋ฆฌ๊ณ  ๋งˆ๋ฌด๋ฆฌ๋กœ typealias๋ฅผ ํ†ตํ•ด ๊ฐ„์ถ”๋ ค์ค€๋‹ค.

enum JSONDefaultWrapper {
    //...
    typealias StringFalse = StringConverterWrapper<JSONDefaultWrapper.TypeCase.StringFalse>
    typealias StringTrue = StringConverterWrapper<JSONDefaultWrapper.TypeCase.StringTrue>
}

๊ทธ๋Ÿผ ์“ธ ์ค€๋น„๋Š” ๋๋‚ฌ๋‹ค.

๊ทธ๋ƒฅ property wrapper 2๊ฐœ ๋งŒ๋“œ๋Š”๊ฒŒ ๋‚˜์„๋ ค๋‚˜?


์“ฐ๋Š” ๋ฐฉ๋ฒ•์€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ด์ „๊ณผ ๊ฐ™๋‹ค.

class Posting: Decodable {
    @JSONDefaultWrapper.StringFalse var stringFalseValue: Bool
    @JSONDefaultWrapper.StringTrue var stringTrueValue: Bool
}


์ด๋ ‡๊ฒŒ ํ•ด์ฃผ๋ฉด ์•„์ฃผ ์ž˜ ๋œ๋‹ค.


์˜ต์…”๋„ ํƒ€์ž…

์ด๋ฒˆ์—๋Š” ์กฐ๊ธˆ ๋‹ค๋ฅธ๊ฑธ ํ•ด๋ณด์ž,

nil๋กœ ๋ƒ…๋‘๋Š”๊ฒŒ ๋‚˜์€ ์ผ€์ด์Šค๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด timestamp๋ฅผ Date๋กœ ๋ฐ”๊พผ๋‹ค๊ฑฐ๋‚˜..?


์ด ๊ฒฝ์šฐ์—๋Š” ๊ธฐ๋ณธ ๊ฐ’์œผ๋กœ ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ๋„ฃ๋Š”๋‹ค๊ฑฐ๋‚˜ ์ด๋Ÿฐ๊ฑด ๋ณ„๋กœ๋‹ค

์ฐจ๋ผ๋ฆฌ nil๋กœ์จ ๋ƒ…๋‘๋Š”๊ฒŒ ๋‚ซ๋‹ค๊ณ  ๋ณธ๋‹ค.

๊ทธ๋ž˜์„œ ์ ์šฉ์„ ํ•ด๋ณด์ž.

๋…๋‹จ์ ์ธ ์ผ€์ด์Šค๋‹ˆ protocol์„ ์ ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.


๋งˆ์ฐฌ๊ฐ€์ง€๋กœ enum JSONDefaultWrapper {} ์•ˆ์— ์„ ์–ธ์„ ํ•ด์ค€๋‹ค.

enum `JSONDefaultWrapper` {
    // Property Wrapper - Optional Timestamp to Optinoal Date
    @propertyWrapper
    struct TimestampToOptionalDate {
        var wrappedValue: Date?
    }
}


์—ฌ๊ธฐ์„œ ๋ด์•ผํ•  ์ ์€ wrappedValue๊ฐ€ ์˜ต์…”๋„์ด๋ผ๋Š” ์ ?

๊ทธ๋ ‡๋‹ด ์ดˆ๊ธฐํ™”ํ•ด๋„ ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค.

์–ด์ฐจํ”ผ nil๋กœ ๋“ค์–ด๊ฐ€๋ฉด ๋˜๋‹ˆ๊น.


(์—ฌ๊ธฐ์„œ struct ์ƒ์„ฑ๊ณผ ๋™์‹œ์— Decodable์„ ๋„ฃ๊ณ  init(from decoder: Decoder)์„ ๋งŒ๋“ค๋ฉด ์ž๋™์œผ๋กœ init()์€ ์ƒ์„ฑ ๋˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.)


๊ทธ ๋‹ค์Œ์€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ

Decoadable ๋ฐ›์•„์ฃผ๊ณ 

extension JSONDefaultWrapper.TimestampToOptionalDate: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let timestamp = try container.decode(Double.self)
        let date = Date.init(timeIntervalSince1970: timestamp)
        self.wrappedValue = date
    }
}


KeyedDecodingContainer ํ™•์žฅํ•ด์ฃผ๊ณ 

func decode(_ type: JSONDefaultWrapper.TimestampToOptionalDate.Type, forKey key: Key) throws -> JSONDefaultWrapper.TimestampToOptionalDate {
    try decodeIfPresent(type, forKey: key) ?? .init()
}

์ด๋Ÿฌ๋ฉด ์ค€๋น„๋.


๋ฆฌ์ŠคํŠธ

์ง„์งœ ์ง„์งœ ๋งˆ๋ฌด๋ฆฌ๋กœ ๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ ๋˜์ง€๊ณ  ๋๋‚ด๊ธฐ

๋ณ„๋„๋กœ ๋žฉํผ ๋งŒ๋“ค ํ•„์š” ์—†๋‹ค. ์ด๋ฏธ ๋‹ค ๊ฐ–์ถฐ ์กŒ๋‹ค.


JSONDefaultWrapperAvailable.TypeCase์— ์ •์˜๋งŒ ํ•ด์ฃผ๊ณ !

enum TypeCase {
    enum List<T: Decodable & ExpressibleByArrayLiteral>: JSONDefaultWrapperAvailable {
        // ๊ธฐ๋ณธ๊ฐ’ - []
        static var defaultValue: T { [] }
    }
}


typealias๋งŒ ์กฐ๋” ์–ด๋ ต๊ฒŒ ์†๋ด์ฃผ๋ฉด

enum JSONDefaultWrapper {
    typealias EmptyList<T: Decodable & ExpressibleByArrayLiteral> = Wrapper<JSONDefaultWrapper.TypeCase.List<T>>
}


์‚ฌ์šฉ ์ค€๋น„ ๋!!



๊ฒฐ๊ณผ

ํ‚ค ๋ฏธ์กด์žฌ


ํ‚ค ์กด์žฌ


์•„์ฃผ ์ข‹์•„ใ…ใ…ใ… ๋“œ๋””์–ด ๋!