iOSでデザイントークンを使ったカラーを運用する方法を模索する

Design Division所属 iOSエンジニアの 藤井 です。弊社はあだ名文化があるため、実は高校時代から使い続けているハンドルネームでもある、「とうよう」というあだ名で普段は呼ばれています。 デザインに理解のあるエンジニアをこの世にもっと増やしていくという理想のもと、普段は日々デザイナーと一丸になっていいデザインをかたちにする仕事をしています。

今回は弊社社内のエンジニア勉強会で知ったデザイントークンという概念から、自分なりに考え研究してみた結果を紹介したいと思います。 デザイナーと共創しているiOSエンジニアの役に立てれば幸いです。

デザイントークンとは

ソフトウェアやWebページをデザインするとき、複数人のチームでもデザインの方針がブレないよう、デザインシステム*1というものを作成することが一般的になってきました。 デザインシステムは、しっかりと定義するだけでも、主観の入りやすい色や、フォント、余白の取り方などを統一でき、デザインに統一感を保つことができるコミュニケーションツールとして機能します。ですが、実際にコミュニケーションをとる上ではシステム自体に加えて、それらを議論やドキュメントで呼称するための名称をつけることが、認識を揃えるために重要になります。

この名称の付け方に一貫性を持たせるため、2016年の10月にSalesforceの開発チームによりデザイントークンという概念が提唱されました。 デザイントークンについてあまり詳細には踏み込みませんが、例えばAdobe社のSpectrumでは色にこのようなラベル(トークン)を定義しています。

Design Tokenの例(Adobe Spectrumより引用)

この中のblue-400cta-background-colorなどがデザイントークンです。

デザイントークンには、全体で共通に使われる文脈に左右されない名前と、各コンポーネントなどで使う際の文脈依存の名前の大きく二階層で名前をつけることが一般的です(この記事では前者を「カラーパレット」、後者を「セマンティクスカラー」と便宜上呼び分けています。)このように名前によって階層を複数に分けることで、色の16進表記やRGB値の値を直接意識することなく、色の統一と置き換えを容易にするということを実現しています。

今回考えたいこと

上記で簡単に説明したデザイントークンで色を管理することは、Webのようにテキストベースで色を管理するときにとても便利です。一例をあげると、Sassなどを用いて以下のように利用されます。

$red-500: #ED323B;

$destructive: $red-500;
$error: $red-500;

こうすることで、

  • 実際の色を使うときには文脈ごとに色を指定でき
  • red-500をまとめて変えたい時は一番上を
  • 文脈ごとの色を変えたい場合は二行目、三行目に指定しているデザイントークンを変える

と、修正の際の柔軟性や修正後の堅牢性を保ったまま変更を行うことができます。 そしてこれは、テキストベースであることによって、デザインツールなどから書き出すこともでき、デザイナーとエンジニアの連携が容易になるというのも魅力のひとつです。

実際には、デザイナーとエンジニアがカラーパレットとして、そのプロジェクトで使いたい色を決めて全体共通のトークンを定義し、その後UIなどの文脈に応じてトークンの名前をつけていくという形で運用されます。

なかなかその場面にあってみないと実感が湧きにくいですが、自分も実際に入っている案件での議論を聞いた時、もともとこのデザイントークンで色を管理しておけばこの議論は回避できたな、ということを感じたことがありました。 そのため、このデザイントークンを用いたカラー運用は特にチーム開発においてとても魅力的だなと思っていました。しかし、そんなことを考えていた時、ある一つの疑問が頭をよぎりました。

「あれ?これってiOSに実装する場合どうするのが正解なんだろう...?」

もちろん、iOSでもコードで色を定義することができるので、それを使えばいいと言ってしまえばおしまいですが、iOSには以下のような色の管理方法の選択肢があります。

  • コードで色の値を指定して書く
  • アセットカタログを使って、カラーアセットとして色を定義する

ダークモードへの対応や、Xcodeにコード上の色のプレビュー機能がないことから、カラーアセットを使うことには大きなメリットがあります。 なによりStoryboardを使っているプロジェクトの場合、GUI上で色をカラーアセットの名前で指定できるようになるため、ミスを防ぐという意味でも欠かせない機能です。 ですがカラーアセット機能には、デザイントークンのカラー運用をするにあたって致命的な欠点があります。それが一度定義したアセットの名前に別の名前をつけられないということです。つまり、仮に#ED323Bという色をred-500と定義したとしても、destructiveなどの名前のカラーアセットを定義したい場合はまた#ED323Bを直接指定しなければいけないということになります。これではデザイントークンで解決したかった課題を解決することができません。

このような理由で、先ほどの疑問にたどりつきました。今回の記事ではこれをどう解決していけばいいのか、ということを考えていきたいと思います。

カラーアセットについて知る

今回のお題について議論していくため、まずはあらためて考える要素についての理解を深めていきたいと思います。 その中でも今回は、さきほど紹介したカラーアセットのメリデメを、もう少し詳しく見ていきたいと思います。

カラーアセットとは?

カラーアセットとは、Xcode11からXcodeに追加された機能です。もともとXcodeには.xcassetsという拡張子のフォルダでアイコンや画像を簡単に管理できる、アセットカタログ機能というものが備わっていました。 そのアセットカタログで色なども管理できるようになったことを、この記事では「カラーアセット」と呼んでいます。

つまり、カラーアセットは実態としてはアセットカタログと同じものであるため、その基本的な使い方はアセットカタログと同じです。 Xcodeの任意のアセットカタログのプラスボタンから 「Color Set」 を選択することで追加できます。

Xcodeのアセットカタログでカラーアセットを追加する方法

カラーアセットの基本的な画面はこのような構成になっています。 (※以降の例では便宜上「GpBlue」、「GpRed」などの名称を使っています。)

カラーアセットの編集画面

一つのColor Setに対しては、主に以下の項目が設定できます。

  • どのデバイスが対象の色になるのか
  • ダークモードとライトモードどちらでも同じ色なのか、違う色を定義するのか
  • 色空間の指定
  • ローカライゼーションの設定
  • 実際の色の情報

これがざっくりとしたカラーアセットの概要となります。

カラーアセットのメリット

続いてカラーアセットのメリットを確認していきましょう。まず画面を見てすぐわかるとおり、その色が実際どのような見た目になっているのかということをGUIで確認できます。これによってデザイナーに「この色であっていますか?」という確認がしやすいのは、一個メリットだと思います。

またGUIで設定が可能なため、macOS標準のColor Panelで色を選ぶことができます。コードだとRGB値を全て0~1に正規化しないといけなかったり、16進表記を使うためにextensionなどで変換処理を実装しなければいけなかったりするため、デザインデータからの持ってきやすさも一つのメリットです。

弊社では公式のカラーパレットが社内配布されています。このように共有されていればなおさら選びやすいですね

そしてもう一つが色に、GUIとコード共通の名前が付けられるという点です。 コードであれば、以下のようにUIKit/SwiftUI関わらず色を呼び出せるようになりますし

let blue = Color("GpBlue")
let blue = UIColor(named: "GpBlue")

何よりXcode内でのGUIから選択可能になるというのは大きな利点です。(以下はSwiftUIでの例ですが、もちろんStoryboardでも使えます)

SwiftUIのGUI色選択にカラーアセットの名称が出てきている様子

これにより「登録している色を、その場で見た目を確認しながら選択する」といったことが可能になります。

最後にあげられるメリットは、ライトモードとダークモードの色を一つの色としてまとめて定義できることです。これは特にダークモードが導入されたiOS13以降では大事な機能です。ダークモード対応をするときにこのColor Setを決めておくだけで、コードで条件などをいちいち書かなくても勝手にダークモード対応できてしまうというのは、工数削減の観点でもすごくありがたい機能です。

カラーアセットのデメリット

では、カラーアセットのデメリットはどこにあるのでしょうか? ここまで見てきた限り一見無いように思います。実際自分自身も少し前までは、ただ便利なものだと思っていました。

ですが、色のデザイントークンについて知った後、デザイントークン的な色彩設計を実現する上で、致命的な欠点があることに気づきました。 それが冒頭にも述べた通り、色の継承機能がないことです。 もう少し噛み砕いて説明します。デザイントークン的な色彩設計の真髄は、「使いたい色をカラーパレットにし、ある文脈で使いたいカラーをセマンティクスカラーにする」というように色を考えるレイヤーを複数に分けることによって、以下の観点を同時に実現できることです。

  • 色の一貫性は崩さない
  • 複数の文脈に同じ色を使ってもメンテナンス性を下げない
  • ある文脈の色の変更を容易にする

そしてこれを実現するためには、セマンティクスカラーを定義する際に、直接色の情報(16進表記やRGBの値)に触れないということが非常に重要です。つまり、例えば「破壊的なボタンの背景色」を定義したいとなった時、直接「赤の値が○○で...」と考えるのではなく「これにはred-400を使用する」というように定義済みのカラーパレットから選べるようになっている必要があります。 しかしこれは「ある名前で定義した色を使って、別の色を定義する」という機能がなくてはいけません。このような機能を色の継承機能と仮に呼ぼうと思います。

ですがカラーアセット、もっというとアセットカタログにはそもそもこのような機能は備わっていません。そのことを理解するためにもう少しカラーアセットの「仕組み」にフォーカスしてみましょう。

カラーアセットの仕組み

さて、ここまでカラーアセットのメリデメをみてきましたが、最後にその内部の構造についてみていきたいと思います。これはアセットカタログ共通の仕組みでもあります。 仕組みを見る方法は簡単で、Assets.xcassetsが実際は拡張子がついたフォルダなので、これをFinderで見てみることでわかります。実際のスクショがこちらです。

Finderでアセットカタログを開いた様子

大まかな構成としては

  • .xcassetsという拡張子のフォルダがある
  • その直下にContents.jsonがある
  • 各アセットごとに特別な拡張子のフォルダがある。色の場合は.colorset
  • 各アセットごとのフォルダの中にContents.jsonがある

画像などのアセットだともう少し複雑ですが、カラーアセットはこのようにかなりシンプルな構成になっており、データは基本的にJSONファイルで表現されていることがわかります。

各ファイルの中身についても見ていきましょう。 まずアセットのフォルダ以外の部分にあるContents.jsonの中には以下のように書かれています。(※アセットカタログの中でフォルダ分けなどすると、フォルダが入れ子になりその度にこれと全く同じContents.jsonが入ります。つまりこれは「そのフォルダがアセットカタログのフォルダですよ」ということを表すファイルだと思われます。)

{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

続いてColor SetごとのContents.jsonはこのような内容が書かれています。

{
  "colors" : [
    {
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0.792",
          "green" : "0.435",
          "red" : "0.042"
        }
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "color" : {
        "color-space" : "display-p3",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0.929",
          "green" : "0.718",
          "red" : "0.463"
        }
      },
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

実際にXcodeで設定できる値が、そのままJSON形式で落とし込まれている様子がわかります。 直接色の情報が組み込まれてしまっている以上、ここに継承の余地は今の所はなさそうです。

もう少し実験してみます。実はカラーアセットには、以下の画像のようにiOS System Colorsなどプラットフォーム側で用意されたカラーパレットから色を指定することもできます

カラーアセットでの色指定の様子

これで試しにdarkTextColorを選択してみましょう。するとContents.jsonの内容はこう変化しました。

{
  "colors" : [
    {
      "color" : {
        "platform" : "ios",
        "reference" : "darkTextColor"
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "color" : {
        "color-space" : "display-p3",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0.929",
          "green" : "0.718",
          "red" : "0.463"
        }
      },
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

reference という新しい項目が出てきました。日本語にすると「参照」、これは継承機能が実際はあるということでしょうか? 実際の仕様を確かめるべく、ここを試しにカラーアセットとして定義している別の色、一例としてGpRedに変えてみました。するとXcode上では以下のような表示になりました。

同じアセットカタログの別の色を指定しようとした場合

透明になっていますね。続いて、新しいアセットカタログを作ってそちらでGpRedを呼び出そうとしてみます。

違うアセットカタログの色を指定しようとした場合

やはり透明になります。このような仕様になっている理由を少し考察してみたものが以下になります。興味のある方はぜひ読んでみてください。

継承ができない仕様になっている理由の考察

まずここで定義した色を、今現在利用できる場所(コードやSwiftUIなど)で使うことを考えてみます。 この場合、「全てのアセットを読み込む」ことで定義されているものを全て取り出せることが保証されるのですごく単純です。 アセットカタログのフォルダの中から、色の情報が入っているフォルダを探して一覧にすればいいだけなので、フォルダの拡張子がアセットであればとりあえず読み込む、という単純な条件で実現ができます。
一方アセットカタログの中から、別のアセットの名前を使おうとするとある問題に差し掛かります。鶏卵問題です。 アセットの中から別のアセットの名前を使うには、アセットカタログ全体を読み込んでいなければいけませんが、そのためにはそのアセットの名前が既にわかっていなければいけません。読み込む際の優先度が決まっていないためです。 別のアセットカタログに定義する場合も同様です。どちらのアセットカタログから読み込めばいいのか分からなければその解釈はできませんし、そもそもお互いに依存しあっている可能性も出てきます。

なおこの考察を踏まえると、おそらく今後も継承機能が実装される可能性はおそらく限りなく低いというのが得られた所感です。そのため、この制約の中でどうデザイントークン的色の管理方法を実現していくか?を考えていくことはかなり価値になってくると思われます。

iOSでの実現方法を考える

今回の主役の一人であるカラーアセットのメリデメを知れたところで、もう一人の主人公デザイントークンのメリデメと組み合わせてその実現方法について探っていきましょう。

一旦カラーアセット以外の手法も考慮して、まずXcodeだけで実現できる方法を考えてみます。パッと思いつく範囲では以下の方法が考えられると思います。

  1. Color Panelにカラーパレットを定義してもらい、それを使ってカラーアセットの指定をしていく
  2. カラーアセットにカラーパレットを定義し、コードでセマンティクスカラーなどの定義をしていく
  3. カラーパレット・セマンティクスカラーのどちらもコードで定義していく

それぞれのメリデメを見ていきましょう

1. Color Panelにカラーパレットを定義してもらい、それを使ってカラーアセットの指定をしていく

まずColor Panelにカラーパレットを定義してもらい、それを使ってカラーアセットの指定をしていくという手法です。これは個人開発においては、既によく見られる手法だと思います。 具体的には、デザインもデベロッパー自身がやる場合にColor Panelにマテリアルカラーなどを登録しておいてその中から色を選ぶことで、全体の雰囲気を統一するというやり方です。

この方法はカラーアセットのメリットを享受したまま色の雰囲気を統一できるため、一番手軽な管理方法だと思います。ですが一方で、以下の問題があります。

  • カラーアセットで指定した後はただの色情報になってしまうので、それがカラーパレットのうちのどの色なのかという情報が抜け落ちてしまう
  • 上に書いた通り、ただの色情報なので、カラーパレットを更新した際全て指定しなおす必要が出てくる

これはお世辞にもデザイントークン的考え方を実現できているとは言えません。よってこの方法は、今回の目的にはあまりそぐわないと言えると思います。

2. カラーアセットにカラーパレットを定義し、コードでセマンティクスカラーなどの定義をしていく

続いての案は、カラーアセットにカラーパレットを定義し、セマンティクスカラーなどはコードで行うという方法です。これは直感的にはかなりいいように思えます。 カラーパレットは実際の色を見ながら定義でき、しかもダークモードとライトモードをひと組で定義できます。 さらにセマンティクスカラーはコードで指定するので、セマンティクスカラーのレイヤーが増えた時も継承といったことが簡単にでき、またコード上でセマンティクスカラーを使う時コード補完を使うこともできます

カラーアセットのアップデートをすれば、自動でパレットの変更が反映されるという点もクリアして一見良さそうです。

ですがこの場合、XcodeのGUIでセマンティクスカラーが使えないという欠点があります。そのため、Storyboardなどでプレビューを見ながら適切な色を選択するといった行為ができなくなってしまいます。 逆にいうとその点に目をつむれば、十分にデザイントークン的考え方を実現できていると思うので、以下のようなプロジェクトで力を発揮するでしょう。

  • SwiftUIのみで作られたプロジェクト
  • Storyboardを使わずコードのみでレイアウトを作っているプロジェクト

3. カラーパレット・セマンティクスカラーのどちらもコードで定義していく

最後の案は全てコードで定義するという方法です。こちらもデメリットは2番と同じで、一方のメリットはというとデザインツールからのエクスポートが比較的作りやすいという点が挙げられると思います。 つまりデザイナーがデザインツール側でカラーパレットやセマンティクスカラーを定義し、それをエクスポートすることで簡単にプロジェクトで使えるようになります。

カラーパレット自体にコード補完がきくことも、場合によってはメリットになるかもしれません。

ですがやはり、Storyboardをプレビューとして活用しUIを組むプロジェクトに不向きなソリューションとなってしまうと思います。

両方のメリットを生かすために

さてここまで見てきた上で、ではStoryboardを使ったプロジェクトでデザイントークン的考え方とカラーアセットの二つの恩恵を受けるにはどうすればいいのか?を考えてみました。 結論、自分は専用のツールを作るしかないなと思いました。幸いXcode ExtensionsやコマンドラインツールなどSwiftで作れるものの幅は広がってきているのでないものは作ってしまえばいいというハッカー精神です。

今回はカラーアセットがフォルダとJSONファイルの集合体というところに着目して、JSONファイルで定義した色からカラーアセットを作成できるコマンドラインツールを作成しました。

具体的には以下のようなpalletscolorFoldersという二つの構造で色を定義できるJSONファイルを用意します。

{
  "pallets": [
    {
      "baseName": "blue",
      "colors": [
        {
          "label": "50",
          "colorContext": "universal",
          "red": 177,
          "green": 210,
          "blue": 237
        },
        {
          "label": "100",
          "colorContext": "light",
          "hex": "0B6FCA"
        },
        {
          "label": "100",
          "colorContext": "dark",
          "hex": "#76B7ED"
        },
        {
          "label": "500",
          "colorContext": "universal",
          "hex": "3097ED"
        },
      ]
    }
  ],
  "colorFolders": [
    {
      "name": "Goodpatch",
      "folders": [],
      "colors": [
        {
          "name": "appKey",
          "colorContext": "universal",
          "value": "blue500"
        },
        {
          "name": "base",
          "colorContext": "light",
          "value": "blue100"
        },
        {
          "name": "base",
          "colorContext": "dark",
          "value": "blue100"
        }
      ]
    }
  ]
}

そしてこれを今回作ったコマンドラインツールで変換すると次のようなカラーアセットが出来上がります。

実際に書き出されるカラーアセット

PalletとSemanticsという大きく二つのフォルダに分かれて実際の色が定義されている様子がわかるかと思います。またJSONファイルでのcolorFoldersでは、直接色の情報を指定しておらず、palletsの方で定義された色名を利用していることを見ていただけると思います。 これにより、特定の文脈で利用している色を変えたい場合はパレットの中の色を選ぶという、デザイントークンのメリットを生かした色管理がカラーアセットでも利用可能になりました。

実際のツールがこちらです。

github.com

現在はα版で、今回のようなニーズを解消するプロトタイプとして作成しましたが、今後はさらに実運用に耐える仕様にアップデートしていきたいと考えています。 また、今回定義したオリジナルのJSONフォーマットをFigmaやSketchなどから書き出せるようにすることなどにも挑戦していきたいです。

まとめ

今回は、デザイントークンという概念から、それをiOS開発においてどう活かしていくかを考え、実際にコマンドラインツールを作成するまで取り組んでみました。 本文をじっくり読んでいただいた方はお気づきかもしれませんが、今回取り組んだ課題はSwiftUIやコードでデザインを組んでいるプロジェクトにおいてはそこまで難しい課題ではありません。 しかしプロジェクトの歴史やチームメンバーの特性から、Storyboardを利用したプロジェクト形態を選ばざるをえない人たちもいると思います。 そのような状況を踏まえると、今回のような取り組みは、自分自身が学生時代から取り組んでいたデザイナーとエンジニアのコミュニケーション支援という観点にも通づるところがあると思いました。

今回取り組んだ部分は細かいところではありましたが、こういったところからひとつずつ異なる職能の「言語」を噛み合わせていき、コミュニケーションコストを下げていくことでチームとしての成果を最大化することに今後も挑戦していきたいと思っています。


Goodpatchではデザイン好きなエンジニアの仲間を募集しています。 少しでもご興味のある方は、ぜひ一度カジュアルにお話ししましょう!

*1:「デザインシステム」という言葉は少し定義が広く、今回扱っている話題においては「デザインガイドライン」という表現が適切ですが便宜上、今回はこの言葉を使用しています。