Localized string set - dynamicMemberLookup

Updated
Sep 29, 2021 10:08 AM
Created
Sep 29, 2021 10:00 AM
Tags
Swift

ローカライズされた文字列の管理は .strings ファイルで管理するのが普通のやり方だが、今回はそれをコードで書くとしたらどうする?というところから脱線した文字列を管理するstorageの実装を紹介

次のように操作できるstructを作った

var strings = Strings()
strings.ja = "こんにちは"
strings.en = "Hello"

これはStringsにja,enというpropertyを実装しているかというと、そうではなく @dynamicMemberLookup を活用したstructになっている

@dynamicMemberLookup
public struct StringsTemplate<LanguageSet> {

  public var storage: [Language : String]

  public init() {
    self.storage = [:]
  }

  public init(_ build: (inout StringsTemplate) -> Void) {
    var instance = Self.init()
    build(&instance)
    self = instance
  }

  public subscript (dynamicMember keyPath: KeyPath<SupportedLanguages, Language>) -> String? {
    get {
      let key = SupportedLanguages.instance[keyPath: keyPath]
      return storage[key]
    }
    mutating set {
      let key = SupportedLanguages.instance[keyPath: keyPath]
      storage[key] = newValue
    }
  }

}

StringsTemplate が受け取るLanguageSetの型が持つpropertyをStringTemplateが持っているように見せることができるようになる。

アプリのなかで取り扱う必要がある言語のセットを定義すると、その対象の言語だけがpropertyとして洗われるようになる

public struct Language: Hashable {

  public let identifier: String
}

public struct SupportedLanguages {

  public static let instance = SupportedLanguages()

  public let ja = Language(identifier: "ja")
  public let en = Language(identifier: "en")
}

最後に、扱いやすく型パラメータを与えたtypealiasをつくる。

typealias Strings = StringsTemplate<SupportedLanguages>

こういうテクニックをiOS15のFoundationにあるAttributedStringは巧みに活用している。

実装の重複や、既存実装の拡張を静的に行うことを実現することにうまく使われていると思う。