์ง๋ํธ์๋ 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 ๋ง๋ค๊ธฐ๊ฐ ์๊ฐ๋ณด๋ค ๋๋ฌด ๊ฐ๋จํด์ ์ ์ฉํ๋ค ์๊ฐ๋๋ ๊ฒ๋ค์
๋ด ์ด๋ฆ์ ๋ฃ๊ณ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ๋ ๋ง๋ค๋ฉด ์ข์ ๋ฏํ๋ค.
๋!