Jiseob Kim

iOS Developer

Swift - SPM ๋งŒ๋“ค๊ธฐ(feat. PropertyWrapperํŽธ)

08 Aug 2021 » Swift


์ง€๋‚œํŽธ์—๋Š” SPM์— ๋Œ€ํ•ด์„œ ๋งŒ๋“œ๋Š” ๊ธฐ๋ณธ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์•˜๋‹ค.


์ด๋ฒˆํŽธ์—์„œ๋Š” ์ง€์ง€๋‚œํŽธ์— ๋งˆ๋ฌด๋ฆฌ๋ฅผ ์ง€์—ˆ๋˜ Property Wrapper๋ฅผ SPM์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ์ ์šฉํ•˜์ž.


์ง€๋‚œํŽธ ํŒŒ์ผ์—๋‹ค๊ฐ€ ์ƒˆ๋กœ์šด swiftํŒŒ์ผ์„ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์ฃผ์ž.


๊ทธ๋ฆฌ๊ณ  ์ง€๋‚œํŽธ์˜ ๊ธ€์—์„œ ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ค์ž.


protocol JSONDecodeWrapperAvailable {
    associatedtype ValueType: Decodable
    static var defaultValue: ValueType { get }
}

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

enum JSWrapper {
    typealias EmptyString = Wrapper<JSWrapper.TypeCase.EmptyString>
    typealias True = Wrapper<JSWrapper.TypeCase.True>
    typealias False = Wrapper<JSWrapper.TypeCase.False>
    typealias IntZero = Wrapper<JSWrapper.TypeCase.Zero<Int>>
    typealias DoubleZero = Wrapper<JSWrapper.TypeCase.Zero<Double>>
    typealias FloatZero = Wrapper<JSWrapper.TypeCase.Zero<Float>>
    typealias CGFloatZero = Wrapper<JSWrapper.TypeCase.Zero<CGFloat>>
    typealias StringFalse = StringConverterWrapper<JSWrapper.TypeCase.StringFalse>
    typealias StringTrue = StringConverterWrapper<JSWrapper.TypeCase.StringTrue>
    typealias EmptyList<T: Decodable & ExpressibleByArrayLiteral> = Wrapper<JSWrapper.TypeCase.List<T>>
    typealias EmptyDict<T: Decodable & ExpressibleByDictionaryLiteral> = Wrapper<JSWrapper.TypeCase.Dict<T>>
    
    // Property Wrapper - Optional Type to Type
    @propertyWrapper
    struct Wrapper<T: JSONDecodeWrapperAvailable> {
        typealias ValueType = T.ValueType

        var wrappedValue: ValueType

        init() {
        wrappedValue = T.defaultValue
        }
    }
    
    // Property Wrapper - Optional String To Bool
    @propertyWrapper
    struct StringConverterWrapper<T: JSONStringConverterAvailable> {
        var wrappedValue: Bool = T.defaultValue
    }
    
    // Property Wrapper - Optional Timestamp to Optinoal Date
    @propertyWrapper
    struct TimestampToOptionalDate {
        var wrappedValue: Date?
    }
    
    @propertyWrapper
    struct TrueByStringToBool {
        var wrappedValue: Bool = true
    }
    
    @propertyWrapper
    struct FalseByStringToBool {
        var wrappedValue: Bool = false
    }

    enum TypeCase {
        // Type Enums
        enum True: JSONDecodeWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - true
            static var defaultValue: Bool { true }
        }

        enum False: JSONDecodeWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - false
            static var defaultValue: Bool { false }
        }

        enum EmptyString: JSONDecodeWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - ""
            static var defaultValue: String { "" }
        }
        
        enum Zero<T: Decodable>: JSONDecodeWrapperAvailable where T: Numeric {
            // ๊ธฐ๋ณธ๊ฐ’ - 0
            static var defaultValue: T { 0 }
        }
        
        enum StringFalse: JSONStringConverterAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - false
            static var defaultValue: Bool { false }
        }
        
        enum StringTrue: JSONStringConverterAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - false
            static var defaultValue: Bool { true }
        }
        
        enum List<T: Decodable & ExpressibleByArrayLiteral>: JSONDecodeWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - []
            static var defaultValue: T { [] }
        }
        
        enum Dict<T: Decodable & ExpressibleByDictionaryLiteral>: JSONDecodeWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - [:]
            static var defaultValue: T { [:] }
        }
    }
}

extension JSWrapper.Wrapper: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.wrappedValue = try container.decode(ValueType.self)
    }
}

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

extension JSWrapper.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
    }
}

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



์กฐ๊ธˆ ๊ธธ์—ˆ๋‹ค.

์•„๋ฌดํŠผ! ์ด์ƒํƒœ๋กœ ๋นŒ๋“œํ•ด๋ณด๋ฉด ์‹คํŒจํ•œ๋‹ค.

CGFloat์„ ์‚ฌ์šฉ ํ•˜์˜€๋Š”๋ฐ, UIKit์„ import ์•ˆํ•ด์คฌ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.


๊ทธ๋Ÿฐ๋ฐ, import UIKit์„ ํ•˜๋”๋ผ๋„ ํƒ€๊ฒŸ์— ๋”ฐ๋ผ ์„ฑ๊ณตํ•  ์ˆ˜๋„ ์‹คํŒจํ•  ์ˆ˜๋„ ์žˆ๋‹ค.


์™œ๋ƒํ•˜๋ฉด ๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด UIKit์˜ ๊ฒฝ์šฐ iOS, tvOS์—๋งŒ ๊ฐ€๋Šฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์ด๊ธฐ ๋•Œ๋ฌธ!

ํƒ€๊ฒŸ์„ Any iOS Device์— ๋งž์ถฐ์ฃผ์ž,


๊ทธ๋ฆฌ๊ณ  ์—ฌ๊ธฐ์„œ๋Š” iOS์—์„œ๋งŒ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ• ๊ฑฐ๋‹ˆ๊น package๋ฅผ ์‚ด์ง ์†๋ด์ฃผ์ž


์˜๋ฏธ๋Š” ๊ฐ„๋‹จํ•˜๋‹ค. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ”Œ๋žซํผ์„ ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ์ •์˜ ํ•ด์ค€๋‹ค.

๋ฐฐ์—ด์— ๋“ค์–ด๊ฐ€๋Š” ์ž๋ฃŒํ˜•์€ SupportedPlatform์œผ๋กœ ๊ตฌ์กฐ์ฒด์ด๋‹ค


์•ˆ์— ์žˆ๋Š” iOS๋Š” enum์„ ์“ธ๊ฑฐ๋ผ ์˜ˆ์ƒ ํ–ˆ๋Š”๋ฐ, ๊ธฐ๊ฐ€๋ง‰ํžˆ๊ฒŒ๋„ ์™„๋ฒฝํžˆ ํ‹€๋ ธ๋‹ค.

  • .iOS: ํƒ€์ž… ๋ฉ”์†Œ๋“œ
  • .v9: ํƒ€์ž… ํ”„๋กœํผํ‹ฐ

๋„ˆ๋ฌด๋‚˜๋„ ์ง๊ด€์ ์œผ๋กœ ํ•ด์„๋œ๋‹ค. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ”Œ๋žซํผ์€ iOS๊ณ  v9์ด์ƒ์—์„œ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค.

๋”ฐ๋ผ์„œ ์„ค๋ช… ํŒจ์Šค.


ํƒ€๊ฒŸ ๋งž์ถ”๊ณ , ํ”Œ๋žซํผ์„ ์ •์˜ํ•ด์ฃผ๊ณ  ๋‚˜์„œ ๋นŒ๋“œ๋ฅผ ํ•˜๋ฉด ์„ฑ๊ณต์„ ํ•˜๊ฒŒ ๋œ๋‹ค.


ํ•˜์ง€๋งŒ,

์ „์˜ ๊ธ€์„ ๋ดค๋‹ค๋ฉด ์‹คํŒจํ• ๊ฒƒ์ด๋ž€ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.


์ ‘๊ทผ์ œ์–ด์ž๊ฐ€ internal๋กœ ๋˜์–ด์žˆ๊ธฐ์— ์ž์‹ ์ด ์“ฐ๋ ค๋Š” ํ”„๋กœ์ ํŠธ์—์„ 

์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ importํ•ด์ฃผ์–ด๋„ ์ ‘๊ทผ์ด ๋ถˆ๊ฐ€ํ•˜๋‹ค.

์จ์•ผํ•  ๊ณณ๋“ค์— ์ ‘๊ทผ์ œ์–ด์ž๋ฅผ public๋กœ ์ˆ˜์ •ํ•ด์ฃผ์ž.

public protocol JSONDecoderWrapperAvailable {
    associatedtype ValueType: Decodable
    static var defaultValue: ValueType { get }
}

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

public enum JSWrapper {
    public typealias EmptyString = Wrapper<JSWrapper.TypeCase.EmptyString>
    public typealias True = Wrapper<JSWrapper.TypeCase.True>
    public typealias False = Wrapper<JSWrapper.TypeCase.False>
    public typealias IntZero = Wrapper<JSWrapper.TypeCase.Zero<Int>>
    public typealias DoubleZero = Wrapper<JSWrapper.TypeCase.Zero<Double>>
    public typealias FloatZero = Wrapper<JSWrapper.TypeCase.Zero<Float>>
    public typealias CGFloatZero = Wrapper<JSWrapper.TypeCase.Zero<CGFloat>>
    public typealias StringFalse = StringConverterWrapper<JSWrapper.TypeCase.StringFalse>
    public typealias StringTrue = StringConverterWrapper<JSWrapper.TypeCase.StringTrue>
    public typealias EmptyList<T: Decodable & ExpressibleByArrayLiteral> = Wrapper<JSWrapper.TypeCase.List<T>>
    public typealias EmptyDict<T: Decodable & ExpressibleByDictionaryLiteral> = Wrapper<JSWrapper.TypeCase.Dict<T>>
    
    // Property Wrapper - Optional Type to Type
    @propertyWrapper
    public struct Wrapper<T: JSONDecoderWrapperAvailable> {
        public typealias ValueType = T.ValueType
        public var wrappedValue: ValueType
        public init() {
            wrappedValue = T.defaultValue
        }
    }
    
    // Property Wrapper - Optional String To Bool
    @propertyWrapper
    public struct StringConverterWrapper<T: JSONStringConverterAvailable> {
        public var wrappedValue: Bool = T.defaultValue
        public init() {
            wrappedValue = T.defaultValue
        }
    }
    
    // Property Wrapper - Optional Timestamp to Optinoal Date
    @propertyWrapper
    public struct TimestampToOptionalDate {
        public var wrappedValue: Date?
        public init() {
            wrappedValue = nil
        }
    }

    public enum TypeCase {
        // Type Enums
        public enum True: JSONDecoderWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - true
            public static var defaultValue: Bool { true }
        }

        public enum False: JSONDecoderWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - false
            public static var defaultValue: Bool { false }
        }

        public enum EmptyString: JSONDecoderWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - ""
            public static var defaultValue: String { "" }
        }
        
        public enum Zero<T: Decodable>: JSONDecoderWrapperAvailable where T: Numeric {
            // ๊ธฐ๋ณธ๊ฐ’ - 0
            public static var defaultValue: T { 0 }
        }
        
        public enum StringFalse: JSONStringConverterAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - false
            public static var defaultValue: Bool { false }
        }
        
        public enum StringTrue: JSONStringConverterAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - false
            public static var defaultValue: Bool { true }
        }
        
        public enum List<T: Decodable & ExpressibleByArrayLiteral>: JSONDecoderWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - []
            public static var defaultValue: T { [] }
        }
        
        public enum Dict<T: Decodable & ExpressibleByDictionaryLiteral>: JSONDecoderWrapperAvailable {
            // ๊ธฐ๋ณธ๊ฐ’ - [:]
            public static var defaultValue: T { [:] }
        }
    }
}

extension JSWrapper.Wrapper: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.wrappedValue = try container.decode(ValueType.self)
    }
}

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

extension JSWrapper.TimestampToOptionalDate: Decodable {
    public 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
    }
}

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


์—ฎ์ด๊ณ  ์—ฎ์ด๋‹ค๋ณด๋‹ˆ ์ƒ๊ฐ๋ณด๋‹ค ๋งŽ์€ ๊ณณ์— public์„ ๋ถ™์—ฌ์คฌ๋‹ค.

๊ฑฐ์ง„ ๋‹ค๋ถ™์˜€๋‹ค๊ณ  ๋ด์•ผํ•  ๋“ฏ ํ•˜๋‹ค.


์ด์ œ ์ปค๋ฐ‹ํ•˜๊ณ  ํ‘ธ์‹œ๊นŒ์ง€ ํ•ด์ฃผ์ž.

์ด๋ฒˆ์—๋„ Branch๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ฒŒ ํ•  ๊ฒƒ์ด๊ณ , ์ด๋ฒˆ ๋ธŒ๋žœ์น˜ ๋ช…์€ Feature/PropertyWrapper๋กœ ์ •ํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๊ฐ€์ ธ์˜ค์ž.


๊ฐ‘์ž๊ธฐ ๋‹คํฌ๋ชจ๋“œ ๋œ๊ฒƒ ๊ฐ™๋‹ค๋ฉด ๊ทธ๊ฒƒ์€ ๊ธฐ๋ถ„ํƒ“.


๊ทธ๋ฆฌ๊ณ , import JSLibrary ํ•ด์ฃผ๊ณ  ํ™•์ธ ์ฝ”๋“œ์™€ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค


// ์ฃผ์„1. wrapper ํ…Œ์ŠคํŠธํ•  ํด๋ž˜์Šค
class TestClass: Decodable {
    @JSWrapper.EmptyString var emptyString: String
    @JSWrapper.IntZero var zeroInt: Int
    @JSWrapper.False var falseBool: Bool
}

์ฃผ์„ 1์˜ ์ฝ”๋“œ๋Š” ์œ„์™€ ๊ฐ™์€๋ฐ, SPM์œผ๋กœ ๋งŒ๋“ค์–ด์ค€ JSWrapper๋ฅผ ์ด์šฉํ–ˆ๋‹ค๋Š”๊ฒŒ ์ค‘์ ์ด๋‹ค.

๊ทธ ์—ญํ• ์€ ์œ„์˜ ๊ฒฐ๊ณผ์™€๋„ ๊ฐ™์œผ๋ฉฐ ์‚ฌ์šฉ๋ฒ•์€ ์ด์ „ ๊ธ€๋“ค์„ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์„ ๋“ฏํ•˜๋‹ค.

๊ฒฐ๋ก ์€ ์ž˜๋œ๋‹ค!


์˜ˆ์ „์— ํ”„๋ ˆ์ž„์›Œํฌ ๋งŒ๋“ค๋‹ค๊ฐ€ ์‹คํŒจํ•œ์ ์ด ์žˆ์—ˆ๋Š”๋ฐ,

SPM ๋งŒ๋“ค๊ธฐ๊ฐ€ ์ƒ๊ฐ๋ณด๋‹ค ๋„ˆ๋ฌด ๊ฐ„๋‹จํ•ด์„œ ์œ ์šฉํ•˜๋‹ค ์ƒ๊ฐ๋˜๋Š” ๊ฒƒ๋“ค์„

๋‚ด ์ด๋ฆ„์„ ๋„ฃ๊ณ  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ•˜๋‚˜ ๋งŒ๋“ค๋ฉด ์ข‹์„ ๋“ฏํ•˜๋‹ค.


๋!