📊

SwiftChartsを利用してよく見るUIをつくってみる ~GraphRangeSlider~

2024/11/06に公開

こんにちは。令和トラベルiOSテックリードとしてNEWT (ニュート) を開発している高橋です!

今回は様々なサービスでよく見る “あのUI” をSwiftChartsを利用して実現したので、その検討過程と実装内容についてお伝えできればと思います。

よくみる “あのUI” について

下図のように「絞り込み時に価格帯とその分布を表現するUI」が、他社旅行系サービスから車販売サービスまで幅広く利用されています。まずは各社のパターンについて洗い出してみました。

Booking / カーセンサー

  • スライダーの移動範囲はグラフの位置に合わせてカクカクと動く。
  • 選択範囲に対してグラフに色がついたりはしない。
  • 範囲が変更されたときにHaptic Feedbackを用いたインタラクションがある(Bookingのみ)

Airbnb

  • スライダーの移動範囲はグラフの決まった位置では止まらず滑らかに動く
  • 選択範囲に対してグラフには色がつく
Booking カーセンサー Airbnb
Booking カーセンサー Airbnb

これらの検討からこのようなUIを実現するには以下のポイントがあることがわかります。

  • 選択範囲のグラフに色がつくかどうか
  • スライダーの動きは滑らかor 指定位置のみ
  • (HapticFeedbackの有無)

上記のポイントについて実装時に考慮が必要となりますが、まずは「グラフをどうつくるか」ということが前提として必要になります。

そこで、iOS16以上から利用できる「SwiftCharts」の使い所ではないかと思い、上記の要件を満たすことができるのか検証してみました。

SwiftCharts

SwiftChartsは、iOS16以上を対象にデータを棒グラフや分布図など様々なパターンに対応したチャートUI構築のためのフレームワークです。グラフはSwiftUIで構築され、フレームワーク内に定義されているChart用のViewを定義することで、簡単に構築することができます。
SwiftCharts

SwiftUI x SwiftCharts での実装

「絞り込み時に価格帯とその分布を表現するUI」を作っていくことを目的として、検討した実装内容をお伝えできればと思います。

NEWTの開発環境

NEWTの開発環境は以下の通り構成されているので、SwiftChartsがiOS16以上対象というところもクリアしています。

  • Xcode16, iOS16+, SwiftUI, Swift5.10 / 6

基本的なグラフの描画方法

単純な棒グラフであれば以下のようにして描画することができます。

struct ContentView: View {
    struct Data {
        let x: Int
        let y: Int
    }

    let data: [Data]

    var body: some View {
        Chart(data, id: \.x) { data in
            BarMark(
                x: .value("x", "\(data.x)"),
                y: .value("y", data.y)
            )
        )
    }
}

Chart がチャートを描画するためのベースとなるViewになります。イニシャライザに表示したいデータの配列を渡します。配列要素の型がIdentifiable に準拠している場合は id: のKeyPathしては不必要です。

Chart のcontentで表示したいチャートタイプのViewを構成します。構成できるViewは以下の7種類です。(SectorMarkはiOS17~で利用可能)

Chart.init(@ChartContentBuilder content: () -> Content) も用意されているので下図のRuleMarkのように複数のMarkを組み合わせてチャートを構成することもできます。

今回の要件では「棒グラフが描画できること」が必要なので BarMark を利用していきます。

AreaMark LineMark PointMark
RectangleMark RuleMark BarMark SectorMark(iOS17~)

今回の実装において必要なこと

上記の例をみると、範囲スライダーとグラフを組み合わせる必要があるため、以下の観点の実現方法を調べてみます。

範囲内・外の区別をしながらグラフの色付けができるかどうか

結論から言うと、可能だが工夫が必要です。

SwiftChartsでは、グラフのX軸に対して選択状態を指定するViewModifierとして、以下の二つが用意されていますが、どちらも iOS17~となっている&グラフに対する選択ジェスチャが前提 でした。

その解決方法として chartForegroundStyleScale(_:) を利用します。

元々は積み上げグラフやグループ化されたグラフなどのスタイル変更に用いるものですが、1つの純粋なグラフに対してもスタイル変更が可能です。

struct ContentView: View {
    struct Data {
        let x: Int
        let y: Int
    }

    let data: [Data]
    let selectedData: [Data]

    var body: some View {
        Chart(data, id: \.x) { data in
            BarMark(
                x: .value("x", "\(data.x)"),
                y: .value("y", data.y)
            )
            /// 1. foregroundStyle(by:)で選択状態に応じたPlottableValueを渡す
            .foregroundStyle(
                by: .value(
                    "status",
                    selectedData.contains(data) ? "active": "inactive"
                )
            )
            /// 2. 1で設定したPlottableValue.ValueをキーとしてColorを紐づける
            .chartForegroundStyleScale([
                "active": Color.red,
                "inactive": Color.blue
            ])
        }
    }
}

不要な凡例や軸線などを消すことができるかどうか

以下のように、ViewModifierに Visibility を渡してチャート自体のスタイル変更が可能です。

struct ContentView: View {
    var body: some View {
        Chart {
            ...
        }
        .chartXAxis(.hidden) // X軸の非表示
        .chartYAxis(.hidden) // Y軸の非表示
        .chartLegend(.hidden) // 凡例の非表示
    }
}

棒グラフの幅を設定することができるか

この要件は、グラフの数が変化することもあり得るので、デザイン的に調整が可能であることがbetterです。

そして、スタイルを変更するために以下のようにBarMarkのイニシャライザに width が用意されています。(height も有り)

この引数には MarkDimension が渡すことができ、Chartをグラフ数で等分した幅に対する割合で決定する .ratio 、固定幅で指定する .fixed 、グラフのinsetで指定する .inset が用意されています。

BarMark(
    x: .value("x", "\(data.x)"),
    y: .value("y", data.y),
    width: .ratio(0.8)
)

ここまでの調査で、グラフ自体は要件通りに描画することができそうです 🎉

スライダーを実装していく

範囲絞り込むようなスライダーは、SwiftUIを用いて比較的簡単に実装することができます。

実装のポイントとして「スライダーの動きは滑らか or 指定位置のみ」というのを前述しましたが、gestureに対するoffsetの計算方法によってどちらでも可能です。

詳しくは後述のOSSの中身を覗いてみてください 👀

GraphRangeSliderをOSSにしてみました

これまで説明した内容を元に、ミニマムな実装で範囲絞り込み x 分布のグラフ表示ができるGraphRangeSlierをOSSにしてみました。

スライダーの詳しい実装などはこちらにありますのでぜひみてみてください!

https://github.com/tomosaaan/GraphRangeSlider

/// 表示させたいデータ要素を定義する
struct Element: GraphRangeElement {
    let x: Int
    let y: Int
}

/// 選択範囲がselectedDataに格納される
/// スライダーは指定位置のみの移動
struct ContentView: View {
    let data: [Element] = [
        .init(x: 10, y: 10),
        .init(x: 20, y: 20),
        ...
    ]
    @State var selectedData = [Element]()

    var body: some View {
        GraphRangeSlider(
            data,
            id: \.x,
            selectedData: $selectedData
        )
    }
}

まとめ

SwiftChartsはただ単にグラフを描画するだけでなく、様々なカスタマイズをすることで既存のUIにも利用できるところがあることがわかりました。

iOSのバージョンによる縛りもありますがグラフを自作するよりも単純に作ることができるので選択肢の一つにいれてみてはいかがでしょうか。

令和トラベルでは一緒に働く仲間を募集しています

令和トラベルでは、一緒に事業成長を牽引いただける仲間を絶賛募集中です!令和トラベルやNEWTに少しでも興味をお持ちいただけましたら、ご連絡お待ちしています。

フランクに話だけでも聞きたいという方は、カジュアル面談も実施できますので、お気軽にお声がけください。

https://www.reiwatravel.co.jp/recruit

11月開催「NEWT Tech Talk vol.12」のお知らせ

令和トラベルでは、定期的に技術や組織に関する情報発信を目的として、「NEWT Tech Talk vol.12」を毎月開催しております!

11月開催は『TypeScriptによるBackend開発 ~開発効率化と運用改善のくふう~』と題して、TypeScriptを採用したBackend開発における、開発効率化や運用改善のための取り組みをご紹介する予定です。オンライン・オフラインのハイブリット開催となりますので、ご興味のある方はぜひご参加ください!

https://reiwatravel.connpass.com/event/334287/

令和トラベル Tech Blog

Discussion