SwiftUI + CoreDataで1週間ごとのデータ登録数が目に見える棒グラフを実装する

# 開発環境
MacOS BigSur 11.4
Xcode 12.4
Swift 5

#概要
記録系のアプリを制作していて、この1週間あたりどのくらいの記録ができたか、棒グラフで可視化できたらいいなーという人向け
↓イメージ画像
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689205/91cf18ed-a3c1-5c58-e366-7ffbc6288eeb.jpeg" width="300px;">

写真は<a href="https://apps.apple.com/jp/app/%E3%83%A9%E3%83%B3%E3%83%8B%E3%83%B3%E3%82%B0-%E3%82%B8%E3%83%A7%E3%82%AE%E3%83%B3%E3%82%B0-%E3%82%A6%E3%82%A9%E3%83%BC%E3%82%AD%E3%83%B3%E3%82%B0-%E3%82%A2%E3%83%97%E3%83%AA-goals/id1136617388">Goals Fitness</a>のものです。





#使用するデータ

今回CoreDateで使用するデータの仕様
エンティティ名: PostEntity

![スクリーンショット 2021-07-18 21.24.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689205/19e65c09-0990-d35a-cbcf-9919e58df767.png)
Date型でデータの追加日を格納する。



#実装

今回は投稿系アプリで1日あたりの投稿数を可視化する棒グラフを実装します。

```BarGraph.swift
import SwiftUI
import CoreData

struct BarGraph: View {
    var body: some View {
            HStack(alignment: .center, spacing: 10)
            {
                ForEach(0..<7){ //6週間前から今週までの投稿数を表示させる。
                    v in
     
                    BarView(value: v) // BarViewに0から6の数字をわたす


                }
            }.padding(.top, 23).animation(.default)
    }

}

```


BarView.swiftではBarGraph.swiftから受け取った値(value)に応じた週のデータ数を表示した棒グラフを生成します。
受け取った値が0なら6週間前、1なら5週間前の登録データ数を表示させます。

```BarView.swift
import SwiftUI
import CoreData

//任意の週の日曜日の0時0分0秒を取得。
func start_of_week(num: Int) -> Date {
    let thisWeekDay = Calendar.current.dateComponents([.weekday], from: Date()).weekday! //今日の曜日を数値で取得（日から土で1~7）
    let n = thisWeekDay - 1
    let start_of_thisweek = Calendar.current.date(byAdding: .day, value: -n, to: Date())! //今週の日曜日を取得
    let week = Calendar.current.date(byAdding: .weekOfMonth, value: num-6, to: start_of_thisweek)! //ここで今日の日付から任意の週巻き戻した日時を取得
    let start_of_date = Calendar(identifier: .gregorian).startOfDay(for: week) //weekで取得した日付の始まりの時刻を取得

    return start_of_date
}

//任意の週の土曜日の23時59分59秒を取得
func end_of_week(num: Int) -> Date {
    let thisWeekDay = Calendar.current.dateComponents([.weekday], from: Date()).weekday! //今日の曜日を数値で取得（日から土で1~7）
    let n = 7 - thisWeekDay
    let end_of_thisweek = Calendar.current.date(byAdding: .day, value: n, to: Date())! //今週の土曜日を取得
    let week = Calendar.current.date(byAdding: .weekOfMonth, value: num-6, to: end_of_thisweek)! //ここで今日の日付から任意の週巻き戻した日時を取得
    let end_of_date = Calendar(identifier: .gregorian).date(bySettingHour: 23, minute: 59, second: 59, of: week)! //weekで取得した日付の23:59:59を取得
    return end_of_date
}


struct BarView: View{
    //BarGraphから受け取る値
    var value: Int
    
    @Environment(\.managedObjectContext) var viewContext
    var postsRequest : FetchRequest<PostEntity>
    
    func getWeekFromDate(num: Int) -> String {
        let thisWeekDay = Calendar.current.dateComponents([.weekday], from: Date()).weekday! //今日の曜日を数値で取得（日から土で1~7）
        let n = thisWeekDay - 1
        let start_of_thisweek = Calendar.current.date(byAdding: .day, value: -n, to: Date())! //今週の日曜日を取得
        let date = Calendar.current.date(byAdding: .weekOfMonth, value: -6+num, to: start_of_thisweek)! //ここで今日の日付から任意の週巻き戻した日時を取得
        let formatter = DateFormatter()
        formatter.locale = .current
        formatter.dateFormat = "M/d"
        return formatter.string(from: date)
    }
    
    //投稿数に応じて棒グラフの長さを返す関数
    func getHeightOfBar(num: Int) -> Int {
        var height = num * 20
        
        if height > 200 { // 投稿数が多くてbarのheightが上限を超えないように関数で処理する
            height = 200
        }
        return height
    }
    
    //BarGraphから受け渡される数値とCoreDataのFetchRequestは初期化処理をする。
    init(value: Int){
        self.value = value
        self.postsRequest = FetchRequest(entity: PostEntity.entity(),
                             sortDescriptors: [NSSortDescriptor(keyPath: \PostEntity.date,
                                                                                        ascending: false)],
                                 
                             predicate: NSPredicate(format:"date BETWEEN {%@ , %@}", start_of_week(num: value) as NSDate, end_of_week(num: value) as NSDate)
                             )
        
            
    }
    
    //指定された日付の投稿を取得 .countプロパティで投稿数を取得
    var postList: FetchedResults<PostEntity>{postsRequest.wrappedValue}
    

    //グラフのビューを生成
    var body: some View {
        VStack {
            VStack {

                ZStack (alignment: .bottom) {
                    RoundedRectangle(cornerRadius: 4)
                        .frame(width: 30, height: 200).foregroundColor(Color(.white)) //グラフの最大の長さ
                    RoundedRectangle(cornerRadius: 4)
                        .frame(width: 30, height: CGFloat(getHeightOfBar(num: postList.count))).foregroundColor(.green) //投稿数に応じたグラフの長さ
                    Text(postList.count.description).font(.footnote).foregroundColor(Color(red:0.64, green:0.5, blue: 0.33))
                    
                }.padding(.bottom, 8)
                
            }
            Text(getWeekFromDate(num: value)) //棒グラフに対応する週の開始日を表示
            
           
            
        }
        
    }
}


```


色々な関数があってごちゃごちゃしていますが、一つずつ見ていきます。

```swift
func start_of_week(num: Int) -> Date {
    let thisWeekDay = Calendar.current.dateComponents([.weekday], from: Date()).weekday! //今日の曜日を数値で取得（日から土で1~7）
    let n = thisWeekDay - 1
    let start_of_thisweek = Calendar.current.date(byAdding: .day, value: -n, to: Date())! //今週の日曜日を取得
    let week = Calendar.current.date(byAdding: .weekOfMonth, value: num-6, to: start_of_thisweek)! //ここで今日の日付から任意の週巻き戻した日時を取得
    let start_of_date = Calendar(identifier: .gregorian).startOfDay(for: week) //weekで取得した日付の始まりの時刻を取得

    return start_of_date
}

func end_of_week(num: Int) -> Date {
    let thisWeekDay = Calendar.current.dateComponents([.weekday], from: Date()).weekday! //今日の曜日を数値で取得（日から土で1~7）
    let n = 7 - thisWeekDay
    let end_of_thisweek = Calendar.current.date(byAdding: .day, value: n, to: Date())! //今週の土曜日を取得
    let week = Calendar.current.date(byAdding: .weekOfMonth, value: num-6, to: end_of_thisweek)! //ここで今日の日付から任意の週巻き戻した日時を取得
    let end_of_date = Calendar(identifier: .gregorian).date(bySettingHour: 23, minute: 59, second: 59, of: week)! //weekで取得した日付の23:59:59を取得
    return end_of_date
}
```

まずは、BarGraph.swiftから受け取る値（0から6）に対して、(num - 6)週間巻き戻した週の日曜日0時0分0秒と土曜日23時59分59秒を取得する関数です。
Swiftの日付操作については以下の記事が参考になりました。
<a href="https://qiita.com/mishimay/items/8d67b583dc6809b2baf5">Swift 3 の日時操作チートシート</a>
<a href="https://qiita.com/kawamurayuto/items/93e9c48174ad080ee973">Swift3 で今日の0時0分0秒を取得する</a>

```swift
init(value: Int){
        self.value = value
        self.postsRequest = FetchRequest(entity: PostEntity.entity(),
                             sortDescriptors: [NSSortDescriptor(keyPath: \PostEntity.date,
                                                                                        ascending: false)],
                                 
                             predicate: NSPredicate(format:"date BETWEEN {%@ , %@}", start_of_week(num: value) as NSDate, end_of_week(num: value) as NSDate)
                             )
    }
```
そして、初期化処理内のFetchRequestでstart_of_weekとend_of_weekで取得した日付の間の投稿を取得します。
取得した投稿をpostListとしておきます。


```swift
RoundedRectangle(cornerRadius: 4)
                        .frame(width: 30, height: CGFloat(getHeightOfBar(num: postList.count))).foregroundColor(.green)
```

そしてView内でpostListの要素数をcountプロパティで取得して、getHeightOfBarという関数に渡します。


```swift
func getHeightOfBar(num: Int) -> Int {
        var height = num * 20
        
        if height > 200 { // 投稿数が多くてbarのheightが上限を超えないように関数で処理する
            height = 200
        }
        return height
    }
```

この関数によって投稿数に応じてheightという数値が返されます。

# 実装結果

![スクリーンショット 2021-07-18 21.20.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689205/9176583d-f9a8-be1a-6703-90a6e86a495e.png)

Previewにテストデータを挿入してグラフを確認。うまくいったようです。

テストデータの挿入コードは以下のようになります。

```BarGraph.swift

struct BarGraph_Previews: PreviewProvider {
    
    static let container = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
    
    static let context = container.viewContext
    
    static var previews: some View {
    let request = NSBatchDeleteRequest(
       fetchRequest: NSFetchRequest(entityName: "PostEntity"))
    try! container.persistentStoreCoordinator.execute(request, with: context)
        
    // テストデータを追加
        
        
    PostEntity.create(in: context,
                      content: "tes", detail: "tes", rate: 3, date: Calendar.current.date(byAdding: .weekOfMonth, value: -2, to: Date())!)
    PostEntity.create(in: context,
                      content: "tes", detail: "tes", rate: 3, date: Calendar.current.date(byAdding: .weekOfMonth, value: -1, to: Date())!)
    PostEntity.create(in: context,
                      content: "tes", detail: "tes", rate: 3, date: Calendar.current.date(byAdding: .weekOfMonth, value: -1, to: Date())!)
    PostEntity.create(in: context,
                      content: "tes", detail: "tes", rate: 3, date: Calendar.current.date(byAdding: .weekOfMonth, value: -1, to: Date())!)
    PostEntity.create(in: context,
                      content: "tes", detail: "tes", rate: 3, date: Calendar.current.date(byAdding: .weekOfMonth, value: -1, to: Date())!)
    PostEntity.create(in: context,
                      content: "tes", detail: "tes", rate: 3, date: Date())
    PostEntity.create(in: context,
                      content: "tes", detail: "tes", rate: 3, date: Date())
    return BarGraph()
        .environment(\.managedObjectContext, context)
            
        
    }
}

```

#参考記事
<a href="https://ichi.pro/swiftui-bogurafu-43218640863280">SwiftUI棒グラフ</a>
<a href="https://qiita.com/mishimay/items/8d67b583dc6809b2baf5">Swift 3 の日時操作チートシート</a>
<a href="https://qiita.com/kawamurayuto/items/93e9c48174ad080ee973">Swift3 で今日の0時0分0秒を取得する</a>