dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

クラシルiOSアプリのリニューアルと新卒iOSエンジニアの奮闘🔥

Top OGP

こんにちは、クラシルiOSエンジニアの uetyo です!

クラシル では、2022年12月に アプリリニューアル を含む、クラシル史上最大規模のブランドリニューアルを実施しました。iOSアプリでは、「ダークモード対応」、「タイポグラフィの再定義・統一」、「アイコン変更」、「カラー定義の全変更」など、大幅なリニューアルを行いました!

この記事は、2022年4月に新卒でdelyに入社し、iOS未経験から数ヶ月の研修を受けた後、アプリリニューアルのためにクラシルのほぼ全ての画面を改修していった裏側についてのお話です 🛠️

この記事の読者対象:

  • アプリリニューアルを控えているデザイナーやエンジニア
  • ダークモード対応時のポイントやハウツーを知りたい方
  • アプリ内のタイポグラフィを再定義して既存画面に適用したい方
  • アプリ内で利用するアイコンを一括変更したい方

カラーの再定義・適用とダークモード対応 🎨🌙

対応期間:4ヶ月

カラー対応のポイント

  • 気合で乗り切る 🔥
  • カラーの管理は Asset Catalog で行う
  • カラーの命名は最重要
  • CGColorを利用する場合は TraitCollection を監視する
  • ダークモード対応のデバッグも TraitCollection で実現できる

カラーの定義

アプリのリニューアル以前は、カラーを多用した華やかなデザインでしたが、それらがあらゆるボタンやテキストに対して使用されていたため、ユーザに認知・行動して欲しいものにまとまりがありませんでした。その結果、ユーザに行って欲しいアクションの学習(メンタルモデルの構築)を促すことができませんでした。

この問題を解決するために、リニューアル時にデザイナーチームがゼロからカラーの定義を再考し、定義外の使用を原則として禁止することになりました。こららの定義は、よりモダンで今後クラシルが目指す食のプラットフォームとしての基盤となるUIを目指して設計されています。

ライトモードで利用するクラシルデザインシステムカラーダークモードで利用するクラシルデザインシステムカラー

実装を始めたところ、思っていたように機能しないカラー定義が発見されました。しかし、後述するカラーの管理方法を採用していたため、途中で何度か定義変更を行っても、最小限のコード変更で対応することができました。

カラーの命名

クラシルの新しいアプリデザインではメインカラーとして13種類を利用します。それぞれの命名はエンジニア・デザイナー双方にフレンドリーな命名になっています。

Content
- 最も利用率の高いカラー郡です。文字やアイコンなどの要素に対して利用するため Content という命名にしています  
- 優先度が高いほうから Primary, Secondary, Tertiary, Quaternary と定義しています  
- PrimaryColor のボタン内に表示するテキストなどは PrimaryInverseColor を利用します

Theme
- ユーザに最もアクションして欲しい場合に利用するブランドカラーです
    
Background / Elevated
- 背景色です。BaseViewの上にホバーするようなコンポーネントを表示する場合は Elevated を利用します。これはすべて Backgound に統一してしまうと著しく視認性が悪いことがあるためです

Fixed
- クラシル内に投稿されたコンテンツ上や常に同じ色を表示する場合に利用します
    
Overlay
- コンテンツにマスク的なものをつける際に利用します

以前は Color.base, Color.state のような何を基準としてベースなのか、ステータスなのか不明な命名となっていたため、エンジニア↔デザイナー間で認識がずれることがありました。しかし、再定義によりで抽象度を高く保ちつつ、より明確な指定ができ、ダークモード・ライトモードのステータスに関係しない命名で設定できるようになりました。

カラーの管理方法

これまでは、UIColorを拡張して独自のブランドカラーを定義していましたが、Xcode 9 (iOS 11)以降では、カラーの管理もAsset Catalog(Color)で行えるようになりました。さらに、Xcode 11 (iOS 13)以降では、ダークモード対応もXcode側で自動的に行ってくれるため、Asset Catalogで管理することにしました。

Asset Catalog contentPrimary

ブランドカラー及びアプリカラーの管理はすべてAsset Catalogで行っています。Asset CatalogはHexColorStyleだけでなく、RGBやOpacityなど、色に関係するプロパティであれば基本的に設定可能です *1

クラシルでは、SwiftGen を使用して、Asset Catalogに定義されたカラーを静的に参照できるようにしています。また、Asset Catalog側でColorの名前空間を設定することで、カラーを指定する際に他のリソース(例えばアイコン)のサジェストが表示されないようにしました。

例:ライトモード( #FFFFFF ), ダークモード( #000000 ) のカラーを Colors.Primary として定義

名前空間の有効化(Provides Namespace を有効化する) Asset Catalog primary color settings

SwiftGenで静的プロパティを生成後、実際のコードで利用する際

// UIKit
Asset.Colors.PrimaryColor.color

// SwiftUI
Asset.Colors.PrimaryColor.swiftUIColor

名前空間はネストさせることも可能なので、普段は利用しないブランドカラー等を Colors.Brand.blueRegular として定義することも可能です

Asset Catalog Sub directory with Provides Namespace

// UIKit
Asset.Colors.Brand.BaseColor.color

// SwiftUI
Asset.Colors.Brand.BaseColor.swiftUIColor

カラーの適用

※ 気合です

Git Branch 戦略
クラシルのiOSアプリはサービス開始から約7年経過しており、現在では100枚以上の画面があります。アプリをリニューアルする際には、すべての画面が最新でメンテナンスが行き届いていることが望ましいですが、難しい状況でした。また、日々大量の変更が発生するため、数ヶ月間リニューアル用のブランチを運用することは不可能だと判断し、画面ごとに分割してリリースすることになりました。しかし、上記で紹介したようにアプリリニューアルに合わせてダークモード対応も行っているため、開発者のみ切り替えれるようにしておく必要がありました。

そこで クラシル iOS ではリリース版はライトモード固定、デバッグ版では設定からライトモードとダークモードを切り替えれるようにすることで開発やQAの効率を向上しました。

アプリのディストリビューション毎に画面モードを固定するには AppDelegate にて UIWindow.overrideUserInterfaceStyle に対して状態を上書きすることでダークモードを無効にすることが可能です。

// For debug version
#if DEBUG
    if isEnabledDebugDarkmode {
        // 端末外観設定に準拠
        UIWindow.overrideUserInterfaceStyle = UIUserInterfaceStyle.unspecified
    } else {
        // ライトモード固定
        UIWindow.overrideUserInterfaceStyle = UIUserInterfaceStyle.light
    }
#endIf

// For public app version
UIWindow.overrideUserInterfaceStyle = UIUserInterfaceStyle.light

// UIUserInterfaceStyle.unspecified = 端末設定に準拠
// UIUserInterfaceStyle.light       = ライトモード固定
// UIUserInterfaceStyle.dark        = ダークモード固定

カラーの適用は後述するタイポグラフィの変更と同時進行で進めました。以前のデザインシステムカラーが多種多用な設定方法(RGB/Hex直書き、Colorの拡張を指定、独自ColorEnumを指定)だったことと、タイポグラフィの変更した画面かどうかをダークモードに対応しているかどうかで判別するためです。

基本的にマッピング等は用いず、一つ一つの画面を調査して色を変更してはビルド→シミュレータで確認を繰り返しました。

アプリカラーリニューアル前とリニューアル後の比較画像

変更を始めた当初は、iOSの実務経験が全くなかったこともあり、意図しないコンポーネントに影響を与えたり、他の画面からカラーの上書きをしていることに気が付かず文字が背景色と混同して読めなくなるなどの問題が発生しました。カラー変更が半分ほど終わると、ドメイン知識や実装経験が身についたため、リファクタリングにも積極的に取り組みました。かなり古い画面になると、カラーの適用だけでも影響範囲が膨大で、どこで状態が変更されているのか不透明で、リーディング力も向上しました。

結果的にこの戦略は膨大な時間を利用することになりましたが、iOSエンジニアとして技術力が幼かった私にとって膨大な画面の実装を読む良い経験になりました。

CGColor で指定されたコンポーネントのダークモード対応

iOS と Xcode Asset Catalog の機能を利用したダークモード対応ではUIColorで指定されているものであれば良しなにOS側が変更してくれますが CGColor を利用しているコンポーネントでは端末の外観設定を切り替えてもアプリを再起動するまで適用されない問題に遭遇しました。調査した結果、ライト・ダークモードが切り替わったことを判定して再適用することで解決することができました!

CGColor を指定している場合は traitCollectionDidChange をオーバーライドすることで外観設定が切り替わったことを判定することができます。このタイミングでCGClorで設定するプロパティを再指定すれば意図した表示にできます。特に layer 系の場合は関係するプロパティも一緒に再指定する必要があります。

// ダーク<->ライトモード切り替え時に適用されないため再設定する
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    layer.borderColor = Const.borderColor.cgColor

    // もし borderWidth も変更する必要がある場合は再指定する
    layer.borderWidth = Const.borderWidth
}

UIButtonにBackgroundImageで指定した際のダークモード対応

AppDelegateでライトモードに固定しているにもかかわらず、端末側の外観設定をダークモードに切り替えると UIButton に image を設定しているコンポーネントのカラーが切り替わる、という問題に遭遇しました。こちらも調査した結果、Color.resolvedColor(traitCollection) は何も指定していなければ UITraitCollection.current を参照してしまい、起動時(AppDelegate)に overrideUserInterfaceStyle を上書きして、アプリ外観設定を固定しても端末の設定に基づいた色を適用してしまうようです。以下のようにbackgroundImageを現在のアプリ外観設定で上書きすることで解決することができました。

public var containerColor: UIColor = .clear {
    didSet {
        setBackgroundImage(
            containerColor.resolvedColor(with: traitCollection).image(),
            for: .normal
        )
    }
}

新タイポグラフィの統一とサイズ変更 🔠

対応期間:4ヶ月

タイポグラフィ変更のポイント

  • 気合で乗り切る 🔥
  • タイポグラフィの定義と命名がとても大事

タイポグラフィの定義と命名

カラーと同様にアプリリニューアルに合わせてタイポグラフィも変更することになったため、こちらも変更します。以前はよくある段階式サイズ指定(title1, subtitle, body, button, caption)と独自指定でした。そのため利用したい場所がタイトルなのに button サイズが利用したい…ニーズに対応できず個別で独自指定することがありました。

新しいタイポグラフィの定義では、この問題を解決するためデザイナーチームがサイズに基づく設計してくれました。

クラシルデザインシステム タイポグラフィ

この設計により、利用範囲が制限されない指定をすることが可能になりました。また命名がAndroidと共通になっているため、例えばAndroidで先行している機能を確認する際にiOS版が作成されていなくても、ほぼ実装できるようになりました。

iOS アプリでは、タイポグラフィの定義を直に指定しています。

public struct NewTypography {
    public static let size36w6 = NewTypography(.w6, fontSize: 36)
    public static let size36w3 = NewTypography(.w3, fontSize: 36)
    public static let size32w6 = NewTypography(.w6, fontSize: 32)
    public static let size32w3 = NewTypography(.w3, fontSize: 32)
    //...
}

// 利用時
let textStyle: TextStyle = NewTypography.size36w6.style

新タイポグラフィの格納

クラシルはアプリリニューアルに付随してアプリ内のタイポグラフィを統一する方針となりました。日本語向けは Hiragino-sans(ヒラギノ角ゴシック) 数字向けは外部フォントを利用することになりました。

Hiragino-sans はiOS標準フォントになるので、別途外部からインポートする必要もないですが、数字向けはiOSには存在しない外部フォントとなるためResourceの一部としてアプリ内に配置、 SwiftGen を用いて静的に参照できるようにしています。

public struct NumberTypography {
    public var style: TextStyle {
        let size = Typography.addFontSizeIfNeeded(size: fontSize.value)
        return TextStyle(
            weight: fontType.weight,
            size: size,
            lineHeightValue: fontSize.lineHeight,
            kerning: fontSize.kerning,
            font: fontType.fontFamily.font(size: size)
        )
    }
// ...

// 利用時
let textStyle: TextStyle = NumberTypography.size32w6.style

タイポグラフィの変更

※ 気合です

こちらもカラー変更と同様に画面毎に変更してはリリースする戦略で進めました。幸い、日本語向けタイポグラフィは以前同様にApple標準 Hiragino-sans だったため、特にフラグなどは用いることなく、差し替えとサイズ調整のみ行いながら進めました。

基本的に定義したTextStyleに置き換えるだけだったので非常にスムーズに進めることができましたが、 UITextViewUISearchBar のダークモード対応+タイポグラフィ変更に関しては、悩む日々を過ごしました… もしタイポグラフィを変更される際は最新のサポートバージョンに合わせて新規でコンポーネントを作成することを強くおすすめします。

アイコン変更 🏷️

対応期間:1ヶ月

アイコン変更のポイント

  • アイコンを含む画像系リソースの格納方法を設計する
  • AssetChangerを用意する
  • 本番リリースしながら確認できるようにする
  • QAチームと特に協力する
  • やっぱり気合 📛

画像系リソースの格納方法を設計する

クラシルでは約300個ほどのアイコンを含む画像系リソースを利用していました。これらのリソースはアプリリリース時から特に格納方法など設計されずに積み上げられていたため似通ったアイコンが重複登録されていることや、ベクター画像(svg)とラスター画像(jpeg, png)がごちゃごちゃに登録されている状態でした。

アプリリニューアルに合わせてアイコンはすべてクラシル独自のものに差し替えることになったため、このタイミングで格納方法についてもゼロから再設計しました。設計の際に気をつけたポイントは以下です。

  1. 利用する画像にView側で着色する必要があるのか分かりやすくする
  2. RenderingModeを Asset Catalog 格納時に指定する
  3. View側で着色する画像に関してはダーク・ライトモードで確認が抜け落ちないようにする

(1)利用する画像にView側で着色する必要があるのか分かりやすくする
こちらは名前空間を用いて画像を指定する際に理解できるようにしました。Colorのとき同様に、AssetCatalog にて名前空間を有効化して登録→SwiftGenを用いて静的に参照できるようにしています。

Asset Catalog 画像の場合もディレクトリに名前空間を利用する(Provides NameSpaceの有効化)

また、着色不要であってもベクター画像の場合は Scales=Single Scale , Resizing=True , SVG 形式で格納することにしました。これは PDF 形式に比べて約50%ファイルサイズを削減することができるためです。さらに、極稀に存在する5MBを超える画像を格納する必要がある場合は PNG 形式で格納します。

Scales, Resizing, SVG を設定して登録した図

以上のように画像を格納することで、どんな画面サイズでも高解像度かつ低容量に扱うことができました。

(2)RenderingModeを Asset Catalog 格納時に指定する
アイコンを格納する際に Image set プロパティから Render As = Template Image を明示的に指定するようにしました。明示的に指定することでView側で色をつける際に .withRenderingMode(.alwaysTemplate) を呼ぶ必要がなくなります。

// in UIKit
// 🙅🏻 Wrong
Asset.Image.hogehoge.image.withRenderingMode(.alwaysTemplate)
// 🙆🏻 Correct
Asset.Image.fugafuga.image

// in SwiftUI
// 🙅🏻 Wrong
Asset.Image.hogehoge.swiftUIImage.renderingMode(.template)
// 🙆🏻 Correct
Asset.Image.fugafuga.swiftUIImage

(3)View側で着色する画像に関してはダーク・ライトモードで確認が抜け落ちないようにする
少しSwift / Xcodeから離れてデザイナーとのやり取りが必要です。

アプリリニューアルに付随してクラシルではダークモード対応を行いました。これまではアイコンを格納する際は黒色で格納していましたが、作業を進める中で以下の問題が発覚しました。

  1. Xcode ダークモードだと Asset catalog に登録したアイコンの識別が難しい
  2. View側で着色を忘れてしまい、QA時にダークモード化した際に着色されていないことが発覚する

Apple純正アイコンのSFSymbolではカラーを指定しない限り 青色( #007AFF ) が適用されます。独自で作成したアイコンもデフォルトカラーとして、この青色を利用することで上記の問題を解決しました

アイコンにはデフォルトカラーとして #007AFF を適用する

AssetChangerを用意する

※ 造語です

本番リリースしながらアイコンを変更するために、AssetChangerというアイコン切り替えシステムを導入しました。これは、本番バージョンでは常に古いアイコンを表示しますが、開発者の場合は新旧アイコンを切り替えることができるものです。システムといっても複雑ではなく、300個の画像系リソースを手動でマッピングすると本番に影響させることなく新アイコンを利用できる、という代物です。

// in Resources module

import SwiftUI
import UIKit

public enum AssetChanger {
    case homeIcon
    case playIcon
    case stopIcon
    // ...

    public var image: UIImage {
        if UserDefaults.standard.bool(forKey: "debug_asset_change") {
            return newAssetImage.image
        } else {
            return oldAssetImage.image
        }
    }

    public var swiftUIImage: SwiftUI.Image {
        if UserDefaults.standard.bool(forKey: "debug_asset_change") {
            return newAssetImage.swiftUIImage
        } else {
            return oldAssetImage.swiftUIImage
        }
    }

    /// リニューアルまで利用するアイコン
    private var oldAssetImage: ImageAsset {
        switch self {
        case .homeIcon:
            return Asset.iconHome
        case .playIcon:
            return Asset.iconPlay
        case .stopIcon:
            return Asset.iconStop24
        // ...
        }
    }

    /// リニューアル後に利用するアイコン
    private var newAssetImage: ImageAsset {
        switch self {
        case .homeIcon:
            return Asset.Icon.iconHome
        case .playIcon:
            return Asset.Icon.iconStart
        case .stopIcon:
            return Asset.Icon.iconPause
        // ...
        }
    }
}

画面数と画像数がとても多いため、それなりに時間がかかりましたが、マッピングと新アイコン適用した際のアイコンサイズ調整以外は基本的に一発置換することで変更できました。SwiftGenで画像を管理していたおかげでかなりスムーズに行うことができました!

アプリリニューアルを振り返って 💭

研修後すぐにアサインされたリニューアルプロジェクトですが、無事に期日に間に合わせて完了することができました!

総プロジェクト期間:5ヶ月
GitHubPR数:250+
扱った画面数:100+(クラシル内のほぼすべてのVC、コンポーネント)

とても作業量の多いプロジェクトでしたが、今後のクラシルの基盤となるUIへアップデートできたこと、これまで乱立していたデザインシステムを最新のものへ統一できたこと、ほぼすべての画面を入社&実務1年以内で経験できたことはとても大きな学びとなりました!

iOS未経験だった私がこのプロジェクトをやり切れたのは、アイコン・クリエイティブ制作、カラー定義などでとてもお世話になったデザイナーの方やレビュー・技術的サポートしてくれたiOSチームメンバー、毎週山のようなチェック項目を確認してくれたQAチームのおかげです!

このプロジェクトが完了したあと約半年経過した現在では、クラシルが目指す新しい方針に向けて新規の機能開発をガシガシと進めつつ、スクラムマスターとしてチームビルディングを行っています。実務2年目も楽しみながらプロダクト・事業に貢献していきます 🔥

関連リンク