🌸

ウェルスナビのiOSアプリのデザインシステムとデザイナー連携の取り組み

に公開

はじめに

こんにちは、サービス機能開発チーム・iOSエンジニアの深来です。
ウェルスナビのiOS・Androidの正社員の人数が増えており、モバイルチーム特集の企画として計6名全員での記事の執筆ができることに喜びを感じる今日この頃です。
私からは、デザイナーとモバイルエンジニア対象のイベントである五反田.mobile Vol.2 - モバイルアプリデザイン最前線で登壇した内容と関連して、ウェルスナビのiOSアプリのデザインシステムとデザイナー連携の取り組みについて紹介します。

デザインシステムとは

そもそもデザインシステムとは、任意のアプリケーションを組み立てるのに用いる再利用可能なコンポーネントと標準規約の集まりのことです。

デザインシステムのメリット

デザインシステムを導入するメリットとしては下記のようなことがあります。

  • 開発スピードの向上: 既存コンポーネントの再利用による効率化
  • 共通認識の確立: デザイナーとエンジニア間での認識齟齬の防止
  • 保守・運用の効率化: 仕様変更への柔軟な対応
  • 一貫したUXの提供: アプリ全体を通じて統一感のある操作感を実現

ウェルスナビのデザインシステム

ウェルスナビのiOS開発では、Figmaのデザインシステムのデザインに合わせたUIコンポーネントやテキストスタイルなどを作成し、iOSのデザインシステムを独立したパッケージとして切り出しています。これにより、デザイナーとエンジニア間でデザインと実装の整合性を保ちやすくなっています。

iOSのデザインシステムの例

実際にウェルスナビのデザインシステムの具体的な実装例をいくつか紹介します。

Color

Figma上では、ブランドカラーを定義したカラーパレットを作成しています。

Figma上でのカラー定義
これらの色をAssetsのColor Setに登録し、以下のような列挙型で定義することで、Figmaと1対1で対応させています。

extension Color {
    public enum wn {
        /// RGB: #282828
        public static let bk1 = Color("bk1", bundle: .module)
        /// RGB: #888888
        public static let bk2 = Color("bk2", bundle: .module)
        /// RGB: #DEDEDE
        public static let bk3 = Color("bk3", bundle: .module)
        /// RGB: #0984E3
        public static let bl1 = Color("bl1", bundle: .module)
        // 省略
    }
}

TextStyle

テキストスタイルについても、Figma上で定義されたフォントサイズ(px)と行間(line)の一覧に基づき実装しています。

FigmaのTextのpxとlineの定義
fontSizeとlineHeightを保持するTextStyle構造体を用意し、ViewModifierを介して適用できるようにしています。

public struct TextStyle {
    var fontSize: CGFloat
    var lineHeight: CGFloat
    var weight: Font.Weight

    public init(
        fontSize: CGFloat,
        lineHeight: CGFloat
    ) {
        self.fontSize = fontSize
        self.lineHeight = lineHeight
        self.weight = .regular
    }
}

public extension TextStyle {
    static var display: Self {
        .init(fontSize: 56, lineHeight: 68)
    }

    static var title1: Self {
        .init(fontSize: 28, lineHeight: 44)
    }

    static var title2: Self {
        .init(fontSize: 22, lineHeight: 33)
    }
    // 省略
}

// MARK: -
public extension Text {
    func style(_ style: TextStyle, color: Color) -> some View {
        font(style.font)
            .lineSpacing(style.lineSpacing)
            .fixedSize(horizontal: false, vertical: true)
            .foregroundStyle(color)
    }
}

public extension View {
    func style(_ style: TextStyle, color: Color) -> some View {
        font(style.font)
            .lineSpacing(style.lineSpacing)
            .fixedSize(horizontal: false, vertical: true)
            .foregroundStyle(color)
    }
}

また、下記のようにKeyPathを使用し、fontのweightを上書きするTextStyleのextensionを作成しています。そうすることでstyle(.display2.bold, color: .wn.bk2)のようにboldを続けて書くことができ、文字の太さの上書きが可能になります。

// MARK: - Weight
extension TextStyle {
    func mutate<T>(
        keyPath: WritableKeyPath<Self, T>,
        value: T
    ) -> Self {
        var copy = self
        copy[keyPath: keyPath] = value
        return copy
    }
}

public extension TextStyle {
    func weight(_ weight: Font.Weight) -> Self {
        mutate(keyPath: \.weight, value: weight)
    }
}

public extension TextStyle {
    var semibold: Self {
        weight(.semibold)
    }

    var bold: Self {
        weight(.bold)
    }
}

上記で作成したColorとTextStyleの共通コンポーネントを使用することで、下記のようにTextやViewにstyleのViewModifierをつけるだけで、直感的に文字の大きさ・太さ・色を指定できるようになります。

Preview
#Preview {
    VStack(spacing: 0) {
        Text("Display")
            .style(.display, color: .wn.bk1)
        Text("Display2")
            .style(.display2.bold, color: .wn.bk2)
        Text("Title1")
            .style(.title1, color: .wn.bk3)
        // 省略
    }
}

ButtonStyle

ボタンについても、Figma上で形状やカラーバリエーションを網羅したボタン一覧を作成しています。

Figmaのボタン一覧
ButtonStyleを拡張したOriginalButtonStyleを作成し、longButtonshortButtonといった独自のViewModifierとして定義しています。これにより、ボタンの装飾(塗りつぶし、枠線、背景なし等)とサイズを簡単に切り替えられます。

public extension View {
    func longButton(_ decoration: ButtonDecoration) -> some View {
        buttonStyle(OriginalButtonStyle(decoration: decoration, length: .long))
    }

    func shortButton(_ decoration: ButtonDecoration) -> some View {
        buttonStyle(OriginalButtonStyle(decoration: decoration, length: .short))
    }
}

public enum ButtonDecoration {
    case line(color: Color) // 枠線のみ
    case fill(color: Color) // 塗りつぶし
    case clear              // 背景なし
}

struct OriginalButtonStyle: ButtonStyle {
    var decoration: ButtonDecoration
    var length: Length

    @Environment(\.isEnabled) private var isEnabled: Bool

    @MainActor
    func makeBody(configuration: Configuration) -> some View {
        switch decoration {
        case let .line(color):
            let color = isEnabled ? (configuration.isPressed ? color.opacity(0.7) : color ) : .wn.bk3
            configuration.label
                .foregroundColor(color)
                .frame(maxWidth: length.size.width)
                .frame(height: length.size.height)
                .background {
                    Capsule(style: .circular)
                        .stroke(color, style: StrokeStyle(lineWidth: 1))
                        .background {
                            Capsule(style: .circular)
                                .fill(.white)
                        }
                }
                // 省略
        case let .fill(color):
            let color = isEnabled ? (configuration.isPressed ? color.opacity(0.7) : color) : .wn.bk3
            configuration.label
                .foregroundColor(configuration.isPressed ? .white.opacity(0.7) : .white)
                .frame(maxWidth: length.size.width)
                .frame(height: length.size.height)
                .background {
                    Capsule(style: .circular)
                        .fill(color)
                }
                // 省略
        }
    }
}

上記の実装のようにButtonのButtonStyleを作成することで、下記のコードのようにView側のコードの実装をシンプルにすることができます。

#Preview {
    VStack(spacing: 8) {
        Button("line / bl1 / enable") { }
            .shortButton(.line(color: .wn.bl1))
        Button("fill / bl1 / enable") { }
            .longButton(.fill(color: .wn.bl1))
        Button("clear / enable") { }
            .shortButton(.clear)
            // 省略
    }
}

デザイナーとエンジニア間で発生した課題

デザインシステムを導入することで、デザイナーとエンジニアで共通認識が確立されました。しかし、それでもなおデザイナーとエンジニア間で発生した課題について紹介します。

  • 最新UIの更新に気づかない
    • 大きいプロジェクトで一度仕様確定後に変更が入った時にデザイナーからエンジニアへの連絡漏れがあり、最新UIの実装漏れのリスクがある

この「連絡漏れ」といったミスを仕組みで解決するため、デザイナーとエンジニアで協議を行いました。

課題解決の取り組み

解決策として、FigmaのDev Modeの機能であるReady for devビューとSlack通知を組み合わせた運用を導入しました。

Ready for devビュー

Ready for devビューでは、開発準備完了となったすべてのデザインが1カ所に集約されます。そのため開発者は、キャンバスをあちこち移動したり複数のページにわたって移動したりすることなく、実装すべき最新のデザインへ即座にアクセスできるようになりました。


変更があったFigmaが枠線で囲まれる
※画像はイメージです

Slack通知の自動化

最新UIの更新に気づかないという課題に対し、ステータスが「Ready for dev」に変更された際、Slackへ通知が自動送信される仕組みを活用することで、デザイナーからエンジニアへの連絡漏れの防止を実現したことで解決できました。

通知の自動化ワークフロー

最新状態へのキャッチアップ漏れを防ぐため、以下の運用を徹底しています。

  • デザイナー: 修正完了後、対象セクションを「Ready for dev」に変更。
  • Slack連携: ステータス変更がトリガーとなり、開発チャンネルへ自動通知。
  • エンジニア: 通知内のリンクから直接「フォーカスビュー」へ飛び、差分を確認。
    このフローの導入により、エンジニアが自律的に更新を検知できる環境が整い、コミュニケーションコストの大幅な削減につながりました。

まとめ

ウェルスナビではデザインシステムの継続的な保守・運用を行うことにより、開発スピード向上やエンジニアとデザイナー間の共通認識を確立しています。
また、課題解決においてもエンジニアとデザイナーが密に連携しており、Figma Dev Mode × Slack通知の自動化といった仕組みを構築することで、さらなる開発効率の向上に努めています。

WealthNavi Engineering Blog

Discussion