5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita全国学生対抗戦Advent Calendar 2023

Day 19

【SwiftUI】Chartsをまだ使ったことがなかったので株価アプリのUIっぽいの作って入門してみた3

Posted at

はじめに

この記事は前回の記事から続いています。
前回は株価をタップ&ドラッグした時のアクションを追加しました。

今回はタップした時の上の株価を表示させる&タップしている株価の位置にsymbolを置くの2つをやってみます。

実際の株価アプリ

RPReplay_Final1702894460.gif

作成したサンプルアプリ

Simulator Screen Recording - iPhone 15 Pro - 2023-12-19 at 13.54.20.gif

準備

前前回のDate拡張と同様

モデルの用意

前前回のモデルと同様

実装

struct ContentView: View {
    // 土日祝日により欠損していたデータをほn
    @State private var stocks: [Stock] = [
        .init(date: .init(year: 2023, month: 10, day: 2), value: 2130),
        .init(date: .init(year: 2023, month: 10, day: 3), value: 4160),
        .init(date: .init(year: 2023, month: 10, day: 4), value: 4308),
        .init(date: .init(year: 2023, month: 10, day: 5), value: 5220),
        .init(date: .init(year: 2023, month: 10, day: 6), value: 6311),
+       .init(date: .init(year: 2023, month: 10, day: 7), value: 6311),
+       .init(date: .init(year: 2023, month: 10, day: 8), value: 6311),
+       .init(date: .init(year: 2023, month: 10, day: 9), value: 6311),
        .init(date: .init(year: 2023, month: 10, day: 10), value: 6311),
        .init(date: .init(year: 2023, month: 10, day: 11), value: 5809),
        .init(date: .init(year: 2023, month: 10, day: 12), value: 5350),
        .init(date: .init(year: 2023, month: 10, day: 13), value: 5196),
+       .init(date: .init(year: 2023, month: 10, day: 14), value: 5196),
+       .init(date: .init(year: 2023, month: 10, day: 15), value: 5196),
        .init(date: .init(year: 2023, month: 10, day: 16), value: 5200),
        .init(date: .init(year: 2023, month: 10, day: 17), value: 6123),
        .init(date: .init(year: 2023, month: 10, day: 18), value: 6110),
        .init(date: .init(year: 2023, month: 10, day: 19), value: 6092),
        .init(date: .init(year: 2023, month: 10, day: 20), value: 6501),
+       .init(date: .init(year: 2023, month: 10, day: 21), value: 6501),
+       .init(date: .init(year: 2023, month: 10, day: 22), value: 6501),
        .init(date: .init(year: 2023, month: 10, day: 23), value: 6867),
        .init(date: .init(year: 2023, month: 10, day: 24), value: 7381),
        .init(date: .init(year: 2023, month: 10, day: 25), value: 7980),
        .init(date: .init(year: 2023, month: 10, day: 26), value: 9016),
        .init(date: .init(year: 2023, month: 10, day: 27), value: 8221),
+       .init(date: .init(year: 2023, month: 10, day: 28), value: 8221),
+       .init(date: .init(year: 2023, month: 10, day: 29), value: 8221),
        .init(date: .init(year: 2023, month: 10, day: 30), value: 7217),
        .init(date: .init(year: 2023, month: 10, day: 31), value: 6221),
    ]

    // 選択している日付
    @State private var selectedDate: Date?
    
+   // 選択している株価
+   @State private var selectedStockValue: Int?

    // チャートの色
    private var chartColor: Color {
        selectedDate != nil ? .cyan : .green
    }

    var body: some View {
        Chart {
            ForEach(stocks, id: \.id) { stock in
                AreaMark(
                    x: .value("日時", stock.date, unit: .day, calendar: .autoupdatingCurrent),
                    y: .value("株価", stock.value)
                )
                .foregroundStyle(.linearGradient(
                    .init(colors: [chartColor.opacity(0.3), chartColor.opacity(0.1)]),
                    startPoint: .top,
                    endPoint: .bottom
                ))

                LineMark(
                    x: .value("日時", stock.date, unit: .day, calendar: .autoupdatingCurrent),
                    y: .value("株価", stock.value)
                )
                .foregroundStyle(chartColor)

                if let selectedDate {
                    // 日付を選択した時に出てくる縦線
                    RuleMark(
                        x: .value("日時", selectedDate)
                    )
                    .foregroundStyle(chartColor)
+                   .annotation(position: .top) {
+                       // 選択している日付の株価を上に表示する
+                       Text(selectedStockValue?.description ?? "???")
+                           .font(.system(size: 13))
+                           .foregroundStyle(chartColor)
+                   }
                }
                
+               if let selectedDate, let selectedStockValue {
+                   // 折れ線グラフと縦線が重なっているところに表示されている丸
+                   PointMark(
+                       x: .value("日時", selectedDate),
+                       y: .value("株価", selectedStockValue)
+                   )
+                   .symbol {
+                       Circle()
+                           .fill()
+                           .frame(width: 20, height: 20)
+                           .foregroundStyle(chartColor)
+                           .shadow(radius: 1)
+                   }
+               }
            }
        }
        .frame(height: 250)
+       // チャートのX軸の範囲
+       .chartXScale(domain: Date(year: 2023, month: 10, day: 2) ... Date(year: 2023, month: 10, day: 31))
+       // チャートのY軸の範囲
+       .chartYScale(domain: 0 ... 10000)
        .chartXSelection(value: $selectedDate)
        .chartGesture { chart in
            DragGesture(minimumDistance: 0)
                .onChanged {
                    // ドラッグしている座標をチャートに知らせる
                    chart.selectXValue(at: $0.location.x)
                }
                .onEnded { _ in
                    // 指を離した時にリセット
                    selectedDate = nil
+                   selectedStockValue = nil
                }
        }
+       .onChange(of: selectedDate) {
+           // nilの場合は無視する
+           guard let selectedDate else { return }
+           // stocksの中で一致する日付を探す
+           selectedStockValue = stocks.first(where: {
+               selectedDate.formatted(.iso8601.year().month().day())
+               == $0.date.formatted(.iso8601.year().month().day())
+           })?.value
+       }
        .preferredColorScheme(.dark)
    }
}

解説

選択中の日付の株価を格納する変数を作成します。

@State private var selectedStockValue: Int?

選択中の日付の株価にPointMarkを配置します。
symbolを使うことによって配置するViewを変更することができます。
今回でいうと、この丸いやつです。
スクリーンショット 2023-12-19 15.11.05.png

if let selectedDate, let selectedStockValue {
    // 折れ線グラフと縦線が重なっているところに表示されている丸
    PointMark(
        x: .value("日時", selectedDate),
        y: .value("株価", selectedStockValue)
    )
    .symbol {
        Circle()
            .fill()
            .frame(width: 20, height: 20)
            .foregroundStyle(chartColor)
            .shadow(radius: 1)
    }
}

選択中の株価をチャートの上に表示しています。
スクリーンショット 2023-12-19 15.17.13.png

RuleMark( /* */ ) {
    //
}
.annotation(position: .top) {
    // 選択している日付の株価を上に表示する
    Text(selectedStockValue?.description ?? "???")
        .font(.system(size: 13))
        .foregroundStyle(chartColor)
}

チャートのX軸とY軸の表示範囲を指定しています。
今回だとX軸は2023年10月2日~2023年10月31日に、
Y軸は0~10000に設定されています。

Chart {
    //
}
// チャートのX軸の範囲
.chartXScale(domain: Date(year: 2023, month: 10, day: 2) ... Date(year: 2023, month: 10, day: 31))
// チャートのY軸の範囲
.chartYScale(domain: 0 ... 10000)

selectedDateからselectedStockValueを検索しています。

.onChange(of: selectedDate) {
    // nilの場合は無視する
    guard let selectedDate else { return }
    // stocksの中で一致する日付を探す
    selectedStockValue = stocks.first(where: {
        selectedDate.formatted(.iso8601.year().month().day())
        == $0.date.formatted(.iso8601.year().month().day())
    })?.value
}

おわり

だいぶ、本物に近づいてきましたね

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?