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

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

https://qiita.com/SNQ-2001/items/3b92fceb72721262a907

https://qiita.com/SNQ-2001/items/73132bf3cdbd4b8bb60b

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

# 実際の株価アプリ
![RPReplay_Final1702894460.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/123885f4-83b9-6457-2ae0-83656fc83966.gif)

# 作成したサンプルアプリ
![Simulator Screen Recording - iPhone 15 Pro - 2023-12-19 at 13.54.20.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/9fc34c0b-8a42-a5d8-3167-ea3719ebca58.gif)

# 準備
前前回の[Date拡張](https://qiita.com/SNQ-2001/items/3b92fceb72721262a907#%E6%BA%96%E5%82%99)と同様

# モデルの用意
前前回の[モデル](https://qiita.com/SNQ-2001/items/3b92fceb72721262a907#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E7%94%A8%E6%84%8F)と同様

# 実装
```diff_swift
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)
    }
}
```

# 解説
選択中の日付の株価を格納する変数を作成します。
```swift
@State private var selectedStockValue: Int?
```

選択中の日付の株価に`PointMark`を配置します。
`symbol`を使うことによって配置するViewを変更することができます。
今回でいうと、この丸いやつです。
![スクリーンショット 2023-12-19 15.11.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/c4698989-7a60-74b1-b22a-a837202141bd.png)
```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/fcb61290-34d5-655e-e436-51fd93146be3.png)

```swift
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に設定されています。
```swift
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`を検索しています。
```swift
.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
}
```

# おわり
だいぶ、本物に近づいてきましたね