다국어 작업을 위해 아래와 같은 형식으로 다국어 값들을 내려받기로 되었다.
{
"id":10000,
"localization": {
"title":{
"ko":"추천",
"en":"For You"
},
"subtitle":{
"ko":"추천",
"en":"For You"
}
}
}
struct Response: Decodable {
let id: Int,
let localization: Localization
struct Localization: Decodable {
let title: [String: String]
let subtitle: [String: String]
}
}
response.localization.title["ko"]
그 말은 매번 모든 struct에 위와 같은 형식을 해줘야 한다는 말인데, 이런 nested property를 좀 더 쉽게 이용할 수 있는 방법이 없을까 해서 고민해봤다.
@dynamicMemberLookup
struct DynamicStruct {
let dictionary = ["someDynamicMember": 325,
"someOtherMember": 787]
// required
// String 말고 KeyPath를 이용할 수도 있음. 자세한 건 아래에서
subscript(dynamicMember member: String) -> Int {
return dictionary[member] ?? 1054
}
}
let s = DynamicStruct()
// let dynamic = s.dictionary["someDynamicMember"] 원래는 이렇게 해야하는 것을
let dynamic = s.someDynamicMember // 이렇게할 수 있다.
// s.someWrongMember 대신 이렇게 오타가 날 수는 있다.
// -> 이건 subscript(dynamicMember)를 string 대신 KeyPath로 이용하면 해결된다.
subscript(dynamicMember:)
를 필수적으로 구현해야 한다. argument 로는 KeyPath, WritableKeyPath, ReferenceWritableKeyPath, ExpressibleByStringLiteral(string)를 이용할 수 있다.\.Car.model
)struct Car {
var model: String
var year: Int
}
var myCar = Car(model: "Sonata", year: 2020)
let modelKeyPath = \Car.model // WritableKeyPath<Car, String>
// WritableKeyPath를 사용하여 값 변경, Car가 class인 경우 ReferenceWritableKeyPath
myCar[keyPath: modelKeyPath] = "Grandeur"
@dynamicMemberLookup
protocol Localizable: Decodable {
associatedtype LocalizableProperty
var localization: LocalizableProperty { get }
subscript<T>(dynamicMember keyPath: KeyPath<LocalizableProperty, T>) -> T { get }
}
extension Localizable {
subscript<T>(dynamicMember keyPath: KeyPath<LocalizableProperty, T>) -> T {
localization[keyPath: keyPath]
}
}
@propertyWrapper
struct Localized: Decodable {
var projectedValue: [String: String]
var wrappedValue: String? {
self.projectedValue[Locale.current.languageCode ?? ""]
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.projectedValue = try container.decode([String: String].self)
}
init(_ value: [String: String] = [:]) {
self.projectedValue = value
}
}
extension KeyedDecodingContainer {
func decode(_ type: Localized.Type, forKey key: K) throws -> Localized {
try decodeIfPresent(type, forKey: key) ?? .init()
}
}
struct Response: Localizable, Decodable {
let id: Int
let localization: LocalizableProperty
struct LocalizableProperty: Decodable {
@Localized
var title: String?
}
}
// 사용할 때는
print(menu.title) // 추천
print(menu.$title["en"]) // For You
import Foundation
@dynamicMemberLookup
protocol Localizable: Decodable {
associatedtype LocalizableProperty
var localization: LocalizableProperty { get }
subscript<T>(dynamicMember keyPath: KeyPath<LocalizableProperty, T>) -> T { get }
}
extension Localizable {
subscript<T>(dynamicMember keyPath: KeyPath<LocalizableProperty, T>) -> T {
localization[keyPath: keyPath]
}
}
@propertyWrapper
struct Localized: Decodable {
var projectedValue: [String: String]
var wrappedValue: String? {
self.projectedValue[Locale.current.languageCode ?? ""]
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.projectedValue = try container.decode([String: String].self)
}
init(_ value: [String: String] = [:]) {
self.projectedValue = value
}
}
extension KeyedDecodingContainer {
func decode(_ type: Localized.Type, forKey key: K) throws -> Localized {
try decodeIfPresent(type, forKey: key) ?? .init()
}
}
struct Response: Localizable, Decodable {
var id: Int
var localization: LocalizableProperty
struct LocalizableProperty: Decodable {
@Localized
var title: String?
}
}
let jsonData = """
{
"id":10000,
"localization":{
"title":{
"ko":"추천",
"en":"For You"
}
}
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let response = try decoder.decode(Response.self, from: jsonData)
print(response.title)
print(response.$title["en"])
https://ilya.puchka.me/decoding-nested-values-with-property-wrappers/
swift macro
init(from:)
을 직접 구현하는 방법으로 해결이 가능하다.init(from:)
함수를 처음부터 다 구현해야 하므로 작업 범위나 난이도가 올라간다. 또한 매크로를 생성하는데 사용되는 swiftSyntax를 새롭게 배워야 하므로 러닝커브가 높다.