カラーパターンを定義して Interface Builder とコードで利用する


Storyboard で UI を作り込んでいる場合、アプリ全体のカラーパターンを変えようと思ったら、意外と大変ですよね?
特に、UI パーツが多かったり、画面がたくさんあると、Interface Builder でひとつひとつオブジェクトを選択して、色を指定していくのは、それなりに時間がかかります。

そこで、カラーパターンを一箇所で定義し、それを Interface Builder とコードの両方から利用できないか検討してみました。

やりたいこと

つまりは色の一元管理です。これができれば、いろいろとメリットがありそうです。

  • アプリ全体のカラーパターンを一括で変更できる。
  • あっちとこっちで同じ色であるべきなのに、少し違っているなどの色指定のミスを減らせる。
  • 定義をもとに色を検索しやすくなり、個々の色の調査、変更が容易になる。

検討したこと

Named color in Asset Catalogs

Xcode 9 からは Asset Catalogs の中に色を定義できて、それをコードと Interface Builder の両方から利用できます。こちらの記事に簡潔にまとまっています。

これが使えれば、即問題解決なのですが、iOS 11 以降でしか使えないので、私の仕事では当分使えそうにありません。残念です。

Color Pallet in Interface Builder

Interface Builder ではカスタムのカラーパレットを作成できます。デザイナーさんからの指示書などをもとにカラーパレットを作っておけば、非常に重宝する機能です。

しかしながら、実際のカラーパレットの情報は Xcode に保存され、アプリのコードから定義名をもとに色を取得することはできません。あくまで Interface Builder 上の入力支援という位置付けです。

R.swift でカラーパターンを定義しつつ、カラーパレットを自動生成するという素敵な記事を見つけました。

しかし、カラーパターンを変更したら、Interface Builder で個々に色を設定し直す必要があります。残念です。

IBInspectable を使ってなんとかする

そんなわけで、ちょうど良い解決策が見つけられなかったので、自分でなんとかする方法を考えました。
概要はこんな感じです。

  1. カラーパターンを UIColor の extension で定義する。
  2. 定義した色を利用できる UI のサブクラスを実装する。

詳細分かりにくいと思いますので、コードを見ていきましょう。

カラーパターンの定義

やり方はいろいろあると思いますが、UIColor の extension でカラーパターンを定義します。

extension UIColor {

    public enum Name: String {
        case CustomRed
        case CustomGreen
        case CustomBlue
    }

    public convenience init(name: Name) {
        switch name {
        case .CustomRed: self.init(red: 1, green: 0, blue: 0, alpha: 1)
        case .CustomGreen: self.init(red: 0, green: 1, blue: 0, alpha: 1)
        case .CustomBlue: self.init(red: 0, green: 0, blue: 1, alpha: 1)
        }
    }
}

ここで enum の型を String にしているのがポイントですが、理由は後述します。

定義した色を利用できるサブクラスの実装

例として UIView の backgroundColor を設定するサブクラスを実装します。

@IBDesignable
class BaseView: UIView {

    private var backgroundColorNameString: String?
    @IBInspectable var backgroundColorName: String? {
        get {
            return self.backgroundColorNameString
        }
        set {
            self.backgroundColorNameString = newValue
            if let nameString = newValue, let name = UIColor.Name(rawValue: nameString) {
                self.backgroundColor = UIColor.init(name: name)
            }
        }
    }

}

enum で色を定義しましたが、IBInspectable では enum はサポートされていません。enum の代替で使えそうなのは、Int と String ですが、後で enum を検索しやすいように String を使っています。

ここで、UIView の extension を使用せずに、サブクラス化しているのにも理由があります。一般的には色の定義名と実際の RGB 値の関係は 1:n で RGB 値から色の定義名を取得することができません。したがって、setter で指定された String 型の定義名を getter で返すため、 stored property が必要になり、extension が利用できません。

Interface Builder では下のように表示され、色の定義名を文字列で指定します。

ちなみにコード上からはこのように利用できます。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = UIColor.init(name: .CustomGreen)
    }
}

まとめ

定義したカラーパターンを利用できるサブクラスを用意することで、色の一元管理が実現できました。

色を設定する必要がある UI パーツ全てに対してサブクラスを用意しなければならないのが難点ですが、それは想像していたよりも気になりませんでした。というのは、枠線の設定や、タッチのヒットエリアの拡張など、既にサブクラスや extension があり、そこにこの色設定の実装を追加することがほとんどだったからです。