XCode で動作環境に応じて API の URL などの設定を変更する

OSX, iOS アプリでも Rails の RAILS_ENV のように、動作環境に応じて値を変えたいという時があります。 この記事では、アプリから利用する Web API の URL を動作環境に応じて切り替えるのを例に、そのやり方を説明します。執筆時の開発環境は下記のとおりです。

  • XCode: Version 7.2
  • Swift: version 2.1.1

1. Build Configuration を作成する

PROJECT -> Info -> Configurations に、動作環境の分だけビルド設定を追加します。例えば、ローカル環境を対象としたビルド設定を追加したいときは、Debug Local と Release Local を追加します。追加するときは、それぞれ、元からある Debug と Release をコピーすると良いでしょう。

Build Configurations

2. Scheme を作成、編集する

Manage Schemes から、動作環境の分だけ Scheme を作成します。また、作成した Scheme それぞれをダブルクリックで編集し、ビルド設定を 1. で作成したものに変更します。例えば、ローカル環境用の Scheme として、AppLocal を作成したら、そのビルド設定は Debug Local および Release Local に設定しておきます。

Scheme

Manage Schemes

Build Configuration の変更

3. コンパイラの設定を変更する

Scheme の作成後は、ソース中で環境ごとに条件分岐ができるように、コンパイラの設定を変更します。PROJECT -> Build Settings 内をいじります。

Objective-C を使う場合は Apple LLVM 7.0 – Preprocessing -> Preprocessor Macros の値を変えます。各動作環境が判別できるように、適当な変数を定義すると良いです。今回の例だと、Debug Local と Release Local に LOCAL=1 を設定します。

Objective-C のコンパイラ設定の変更

Swift を使う場合は Swift Compiler – Custom Flags -> Other Swift Flags の値を変えます。 -D <flag> の形式で <flag>true にできるようなので、そのように書きます。今回の例だと -D LOCAL を追加すると良いです。また、複数の値を設定する場合は -D DEBUG -D LOCAL のように書くことができます。

Swift の方にはなぜか DEBUG の値が設定されていないため、ついでに設定しておくと良いと思います。

Swift のコンパイラ設定の変更

4. Conditional compilation statement を書く

Conditional compilation statement で条件分岐させて、開発環境ごとに異なる値を変数に入れてやります。

Objective-C の場合は以下のように書きます。

#ifdef LOCAL
  NSString * const kAPIBase = @"http://localhost.example.com/api/";
#else
  NSString * const kAPIBase = @"http://production.example.com/api/";
#endif

Swift の場合は以下のように書きます。

#if LOCAL
  let kAPIBase = "http://localhost.example.com/api/"
#else
  let kAPIBase = "http://production.example.com/api/"
#endif

参考

指定した行数でテキストを省略できるライブラリ Truncator を作った

N 文字目以降を省略するというライブラリはたくさんあるのですが、行数指定できるものは見かけないので作りました。N 行以上になった時は省略したいというケースは結構ある気がするんですが、なぜそういうライブラリは無いのだろう……。

Truncator – NPM
Truncator – Github

使い方

truncate(el, text, { line: 3, ellipsis: '……' });

のような感じで使います。この例だと、要素 el に文字列 text を入れて、それが 3 行に収まるように省略します。また、省略記号は ...... を指定してます。

アルゴリズム

以下のような感じのアルゴリズムで動いてます。
1. el の一行の高さ L を取得する。
2. 行数 n * L で目標の高さ H を算出する。
3. eltext を入れてその高さ h を算出する。
4. h <= H を満たす、最大の省略位置を二分探索し、省略後の文字列を el に入れる。

一行の高さを取得する

一行の高さは window.getComputedStyle(el).lineHeight で簡単に取得できる……と思いきや、normal とかが返ってくるケースがあるので、工夫する必要があります。 Truncator では、対象の要素に適当な一文字を入れた時の高さを一行の高さとして扱っています。

省略すべき位置を二分探索する

単純に、center = (left + right) / 2 して、text.substring(0, center) し、h <= H だったら left を center に、そうでなければ right を center にする二分探索です。ただ、h <= H だったときでも center が解ではないとは限らないため、次の探索でも center を探索空間に含めたままにする必要があります。

最後に

バグ報告大歓迎です!

Bookshelf のアソシエーションで発生する循環読み込みによるエラーを回避する

Node.js の ORM である Bookshelf では公式サイトの例のようにアソシエーションを定義することができます。 しかし、モデルの定義を別ファイルに分けるとこの例が動かなくなります。 この記事では、モデルの定義を別ファイルに分けても意図通りに動くコードの書き方の説明と、それを支援する Bookshelf プラグインの紹介をします。

循環読み込みする例

以下のコードは page.js 内で book.js を読み込み、 book.js 内で page.js を読み込んでおり、循環読み込みが発生しています。 例えばこの状態で、別のファイルから page.js を読み込むと、 book.js 内の require('./page'){} を返し、 Book モデルの pages アソシエーションを使用するときにエラーが発生します。

// book.js
const Page = require('./page');

const Book = bookshelf.Model.extend({
  pages: function() {
    return this.hasMany(Page);
  }
});

module.exports = Book;
// page.js
const Book = require('./book');

const Page = bookshelf.Model.extend({
  book: function() {
    return this.belongsTo(Book);
  }
});

module.exports = Page;

解決策

この問題については、生成したモデルを保持するオブジェクトを別で定義するというのが一般的な解決策だと思います。 つまり、以下のようなコードになります。 以下のコードは、 require の循環読み込みが発生しているものの、実際にモデルが利用されるときには models 以下に定義したモデルが格納されているため、意図した通りの動作になります。

// book.js
const models = require('./models');

require('./page');
const Book = bookshelf.Model.extend({
  pages: function() {
    return this.hasMany(models.Page);
  }
});

module.exports = models.Book = Book;
// page.js
const models = require('./models');

require('./book');
const Page = bookshelf.Model.extend({
  book: function() {
    return this.belongsTo(models.Book);
  }
});

module.exports = models.Page = Page;
// models.js
exports = {};

あらかじめ全て読み込んでおく例

上記の解決策でも動くのですが、各モデルのファイルで依存するファイルを読み込む必要があります(require してる部分)。なぜなら、あるモデルを利用する時に、依存する他のモデルの生成が完了しているとは限らないためです。 モデルの数が増えてくると、依存関係を書くのがめんどくさくなってくると思います。また、漏れがあった時に気づくことが困難です。

よって、 models.js 内であらかじめモデルをすべて読み込んでおくのも良いのではないかと思います。 各モデルのファイルから依存するモデルを読み込む require を削除し、 models.js を以下のようにします。 MODEL_DIR には、各モデルのファイルが入っているディレクトリを指定します。各モデルのファイルと models.js の間で循環読み込みしていますが、 exports = {}; の後に require しているため、 require('./models') は意図したオブジェクトを返してくれます。

// models.js
exports = {};

const fs = require('fs');
const MODEL_DIR = './';
fs.readdirSync(MODEL_DIR)
  .filter((filename) => 'models.js' !== filename)
  .forEach((filename) => {
    require(MODEL_DIR + filename);
  });

ES6 Proxy を使って必要なときに読み込む例

すべて読み込むのではなく、必要となったタイミングで読み込むようなコードは以下のようになります。 Proxy を使わなくても models('ModelName') でモデルを返すような関数を定義すれば同じようなことは実現できます(文字列で書きたくなかった)。Node.js ではまだデフォルトで Proxy を使えない点と、最新の記法を使うために Polyfill が必要な点、また、モデル名をファイル名に変換する処理が必要な点に気をつける必要があります。(執筆時の Node.js の最新版は 5.4.0)

const _ = require('lodash');
const MODEL_DIR = './';

const proxy = new Proxy({}, {
  get: function(models, name) {
    return models[name]
      || (models[name] = require(MODEL_DIR + _.snakeCase(name));
  }
});

exports = proxy;

registry プラグイン

上記のようなことをやってくれる Bookshelf のプラグインが標準で提供されています。

Plugin: Model Registry · tgriesser/bookshelf Wiki

bookshelf.model('ModelName', Model) でモデルを登録し、 this.hasMany('ModelName') のように、アソシエーションの時に文字列でモデル名を指定することができます。 しかし、アソシエーション以外で、モデルを利用する時に、 bookshelf.model('ModelName') と書くのがめんどくさいです。個人的には自前で書いてしまう方が良いように思います。

まとめ

Bookshelf のモデルの定義を別ファイルに分けると、単純な書き方では循環読み込みでエラーが発生するようになります。 この問題は、生成したモデルを保持するオブジェクトを別で定義することで解決することができます。 モデルを定義する作業を楽にするために、モデルのファイルをあらかじめすべて読み込む処理を書いたり、モデルが必要となったタイミングで動的に読み込む処理を書くのも有効です。 また、Bookshelf 標準でこういった問題を解決してくれる registry というプラグインが提供されています。