8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ビルド環境で変わる変数の管理をYAMLベースにしてみた

Last updated at Posted at 2018-12-20

はじめに

ビルド環境によって値を変えたいシーンってよくありますよね。
これまでにも色々なところで様々なアプローチが提案/実施されてきたと思いますが、自分の中でようやく1つの答えに辿り着いた気がするので共有します。

これまで

まず、自分がこれまでiOS開発で関わってきた中でどのように実装してきたかを振り返ります。

Objective-C時代

HogePrefix.pch
#ifdef DEBUG
#define HOGE @"foo"
#else
#define HOGE @"bar"
#endif

ObjCといえばプリプロセッサマクロですね。
~Prefix.pchなんてのはObjC時代からやってるエンジニアには馴染み深いファイルだと思います。
この~Prefix.pchに↑のような#ifdef+#defineが大量に列挙されて節子FatViewControllerも真っ青な一大ファイルの出来上がり!(死

~Swift3.x

Define1.swift
#if DEBUG
let hoge = "foo"
#else
let hoge = "bar"
#endif

let fuga: String = {
  #if DEBUG
  return "foo"
  #else
  return "bar"
  #endif
}()

型について厳しくなったため多少はマシになりましたが、
結局内部でマクロによって分岐するしそもそも値がコードに入り込んでいるという点で本質的にはほとんど変わらないですね。

Build SettingsUser-Definedでビルド毎に違う値をセットするという方法もありますが、この方法はInfo.plistが汚れるので個人的にあまりやりたくありません。

そこで、別のファイルに実際の値を外出しし、コード側でそのファイルを読み込むことにより、
環境毎に読み込むファイルを変えるだけで環境の切り替えをできるようにしました。

当時は**SwiftyJSON**が全盛だったこともあり、

Config-Dev.json
{
  "hoge": "foo"
}
Config.json
{
  "hoge": "bar"
}
Config.swift
let config: JSON = {
    let file: String = {
        #if DEBUG
        return "Config-Dev"
        #else
        return "Config"
        #endif
    }()
    let path = Bundle.main.path(forResource: file, ofType: "json")!
    let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))
    return JSON(data: data)
}
let hoge = config["hoge"].stringValue

という感じでやっていました。

これはSwiftJSONJSONsubscriptでもJSONを返すことを利用して、
ネストが深くなってもjson["key1"]["key2"][0].stringValueみたいにシンプルに呼べるというメリットがあったんですが、それ以上にsubscriptのキーで事故るリスクが大きいという問題がありました。
Stringを直打ちするのでキーの補完も効かないからキー名が長ければ長いほど地獄...

Swift4~

Swift4になってCodableが登場したことで、同じ構造のstructを定義してそこにファイルを流し込む
ことにより、先のSwiftyJSONを使うメリットを踏襲しつつデメリットも大幅に削減できるようになりました。

Config.swift
struct Config: Codable {
  let hoge: String
}

let config: Config = {
    let file: String = {
        #if DEBUG
        return "Config-Dev"
        #else
        return "Config"
        #endif
    }()
    let path = Bundle.main.path(forResource: file, ofType: "json")!
    let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))
    return try! JSONDecoder().decode(Config.self, from: data)
}

let hoge = config.hoge

振り返り終わり

さて、ここまででもかなり変数管理は楽になったのですが、新たな問題(というか欲)が出てきました。

  1. 読み込ませるファイルをマクロ(#if DEBUG)で切り替えるのをやめたい
  2. 環境では変わらない値(APIのパスとか)も外出ししたい(定数値管理)
  3. 管理する値の種類毎にファイルを分けたい(単一ファイル管理時のコンフリクト防止)
  4. どうせならYAMLで書いた設定ファイルをマージして設定ファイルを生成するようにしたい
  5. Config.swiftがボイラープレートになりそうだから自動生成させたい

ということで、これらを一気に実現するためにBuildConfig.swiftというCLIツールを作りました。

BuildConfig.swift

YAMLやJSONで書かれた設定ファイルをマージしてPListを生成します。
もちろん、環境に応じて読み込むファイルを切り替える仕組みも用意しています。
更に、そのPListの構造を保持したSwiftコードを自動生成します。

使い方

CocoaPodsでインストールします。

Podfile
pod 'BuildConfig.swift', '~> 2.0'

こんな感じでファイルを用意します(例)

$(SRCROOT)/Resources/Config/API.yml
API:
  path:
    login:
      method: POST
      path: /auth
    getList:
      method: GET
      path: /list
$(SRCROOT)/Resources/Config/Link.yml
Link:
  twitter: https://twitter.com/417_72ki
  github: https://github.com/417-72KI
  qiita: https://qiita.com/417_72ki
$(SRCROOT)/Resources/Config/.env/staging.yml
API:
  domain: https://localhost
  
$(SRCROOT)/Resources/Config/.env/production.yml
API:
  domain: https://foo.bar.co.jp
  

Build PhaseにRun Scriptを追加します

if [ "${CONFIGURATION}" = 'Release' ]; then
ENVIRONMENT='production'
else
ENVIRONMENT='staging'
fi

${PODS_ROOT}/BuildConfig.swift/buildconfigswift -o ${SRCROOT}/Libs/BuildConfig_swift -e $ENVIRONMENT ${SRCROOT}/Resources/Config

-oで生成ファイルの出力先、-eで読み込む環境を指定し、最後に読み込むymlファイルがまとまっているディレクトリを指定します。
.envフォルダの下に置くファイルは、-eで指定する環境と同じ名前にしてください。

また、Run ScriptのInput Filesに
$(TEMP_DIR)/buildconfigswift-lastrun
、Output Filesに
$(SRCROOT)/Libs/BuildConfig_swift/BuildConfig.generated.swift

$(SRCROOT)/Libs/BuildConfig_swift/BuildConfig.plist
をそれぞれ追加します。
(※生成先に制約はありませんが、-oで指定した出力先とOutput Filesのパスは一致するようにする必要があります。)

image.png

あとはビルドするだけ

こんな感じでファイルが生成されます。

`$(SRCROOT)/Libs/BuildConfig_swift/BuildConfig.plist`
$(SRCROOT)/Libs/BuildConfig_swift/BuildConfig.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>API</key>
        <dict>
            <key>path</key>
            <dict>
                <key>login</key>
                <dict>
                    <key>path</key>
                    <string>/auth</string>
                    <key>method</key>
                    <string>POST</string>
                </dict>
                <key>getList</key>
                <dict>
                    <key>path</key>
                    <string>/list</string>
                    <key>method</key>
                    <string>GET</string>
                </dict>
            </dict>
            <key>domain</key>
            <string>https://localhost</string>
        </dict>
        <key>Links</key>
        <dict>
            <key>twitter</key>
            <string>https://twitter.com/417_72ki</string>
            <key>qiita</key>
            <string>https://qiita.com/417_72ki</string>
            <key>github</key>
            <string>https://github.com/417-72KI</string>
        </dict>
    </dict>
</plist>
`$(SRCROOT)/Libs/BuildConfig_swift/BuildConfig.generated.swift`
$(SRCROOT)/Libs/BuildConfig_swift/BuildConfig.generated.swift
import Foundation

struct BuildConfig: Codable {
    static let `default`: BuildConfig = .load()

    let API: API
    let Links: Links

    enum CodingKeys: String, CodingKey {
        case API
        case Links
    }
}
// 以下略

BuildConfig.plistは実際にはバイナリ形式で出力されます。

呼び出し

BuildConfig.defaultでルートオブジェクトにアクセスすることができます。
あとは、YAMLに書いていたキーをドットで繋いでいけば目的の値が読み出せるはずです。
例: BuildConfig.default.API.path.login.path => /auth

実際に使ってみて

ちょうど今スクラッチ開発の案件が続いているので、それに組み込みつつ改修をかけています。

良かったこと

  • yamlで管理できるようになったことで設定値が見やすくなった
  • ファイル分割できるようになったことで値が探しやすくなった
  • 自動生成されるファイルは当然.gitignoreに登録するので、コンフリクト地獄も解消
  • Info.plistを汚すことなく設定値を増やしたり減らしたりできる
    • 当然キーのtypoによる余計なバグも無くなる!
  • #if DEBUGからの脱却
  • BuildConfig.plistをバイナリ化したことである程度軽量化できた

別途生まれたつらみ

  • 新しいビルドシステムでビルドに失敗したり値が更新されなかったりする事象が発生
    R.swiftがちょうど同じ問題に直面していたため、その解決方法を拝借してクリアしました。
  • Xcodeではyamlを直接開けないため、デュアルディスプレイが半必須になった
    とはいえ、デュアルディスプレイが前提の開発環境なら問題にならないし、シングルでも値が散らばって探す手間を考えたら個人的にはマシかなと思ってます。
    今は外部ディスプレイでXcode、MacbookのメインディスプレイでVSCodeを開いて作業しています
  • yamlのパーサが手強い
    yamlの仕様上、スカラー値が全てStringでも取れるため、型の判定処理がかなりつらいことになっています(今もDoubleが上手く取れてなくて😱なことに...)
    あとPListでサポートされている型の一部の対応がまだできていないのも課題...

最後の課題についてはちょうど今スクラッチ開発が続いているので、組み込みながら改修していきたいと思ってます。

まとめ

  • ビルド環境で変わる値はファイルに外出しできると幸せになれる😇
  • 自動生成最高!
  • BuildConfig.swift使ってね!(

余談

こういう値って環境変数って呼んでいいものなのだろうか🤔

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?