kitoko552.memo

kitoko552のメモ

SwiftGenでリソースをタイプセーフに扱う

概要

SwiftGenを使えばLocalizable.strings, Storyboard, ImageAssetsなどのリソースをenum化してタイプセーフに書けるよって話です。

github.com

SwiftでStringを扱うときの問題点

SwiftでStringを普通に扱うと以下のようになります。

let title = "What the fuck"

Localizeする場合は以下のようにLocalizable.stringsに記入したキー文字列を使ってStringを扱います。

// Localizable.strings

"AlertTitle" = "What the fuck";
let title = NSLocalizedString("AlertTitle", comment: "")

これだとダサいので僕が所属しているプロジェクトでは以下のように書いていました。

extension String {
  static func localized(key key: String) -> String {
    return NSLocalizedString(key, comment: "")
  }
}

let title = String.localized(key: "AlertTitle")

これでもいいですが、キー文字列はStringなので誤字る可能性がありますよね。
そしてStringなので誤字ってもコンパイラはエラーは吐いてくれません。
Localized Stringの場合は指定した文字列に当てはまるキー文字列がない場合、指定した文字列がそのまま採用されてしまいます。

let title = String.localized(key: "alertTitle") // aが小文字
// titleには"alertTitle"という文字列が入る

SwiftGenはこの問題を解決してくれます。
SwiftGenは、Localized.stringsをenum化してくれて、タイプセーフな書き方を実現してくれます。

SwiftGenの使い方

SwiftGenはhomebrewに対応しているので簡単にインストールできます。

$ brew install swiftgen

あとはswiftgenコマンドを実行するだけです。
enum化したいリソースに合わせてコマンドを実行します。
Localizable.stringsの場合は以下のようなコマンドを実行してファイルを作成することができます。

$ swiftgen strings --output /path/to/outputfile /path/to/Localizable.strings

他のリソースに関する使い方はSwiftGenのページに書いてます。

SwiftGenを使うと

例えば、上に書いたLocalizable.stringsにSwiftGenを使うと、以下のようなファイルが作成されます。

enum L10n {
  case AlertTitle
}

extension L10n : CustomStringConvertible {
  var description : String { return self.string }

  var string : String {
    /* Implementation Details */
  }
  ...
}

func tr(key: L10n) -> String {
  return key.string
}

そうすると、Stringは以下のように扱えるようになります。

let title = L10n.AlertTitle.string
// or
let title = tr(.AlertTitle)

こうすることで、もし該当するenum caseがないとコンパイラがエラーを吐いてくれるため、間違った文字列が採用されてしまうことはありません。
これでリソースがタイプセーフに扱えるようになりました!
Swifty!!!

Assets Libraryを扱うときの問題点

Assets Libraryを使ったUIImageは以下のようにインスタンス化します。

let image = UIImage(named: "compose")

この際の難点は、 - 引数がString - 返り値がOptional

の2点です。
引数がStringで困る理由は、上のLocalizable.stringsと同じで、誤字ってもコンパイラがエラーを吐いてくれないという点です。
そして返り値がOptionalで困る理由は、単純にめんどくさいからです。

Optional型を渡しても大丈夫なメソッドやプロパティは問題ありませんが、Optionalで渡せないものの場合は当然if letguardnilでないことを保証してから値を渡さなければなりません。

// 例えば引数がUIImageのメソッドを使う場合
func trimImage(image: UIImage) {
  // Do something
}

// if let や guard でnilでないことを保証しなければならない。
if let image = image {
  trimImage(image)
}

SwiftGenを使うと

SwiftGenはAssets Libraryにある画像もenum化できます。
SwiftGenを使うと、以下のようなファイルが生成されます。

extension UIImage {
  enum Asset : String {
    case Compose = "compose"
    case Apple = "apple"
    case Banana = "Banana"

    var image: UIImage {
      return UIImage(named: self.rawValue)!
    }
  }

  convenience init!(asset: Asset) {
    self.init(named: asset.rawValue)
  }
}

こうすると、Assets Libraryを使ったUIImageを以下のようにインスタンス化することができます。

let image = UIImage(asset: .Compose)

これでタイプセーフかつOptionalでないUIImageのインスタンスを取得できるようになりました!
Swifty!!!

終わり

SwiftGenはStorybaordやUIColorもenum化しできますが割愛します。
使い方等はSwiftGenのページに行けば書いてあるのでそちらを参照してください。

終わり。