NCMBのSwift SDKを使って日報アプリを作る（その3：日報データの検索と一覧/詳細表示）

NCMBのSwift SDKを使ってデモアプリを作ってみます。今回は業務系でよくあるニーズの日報アプリを作ってみます。実際にはデータストアやファイルストアを使うので、応用すれば汎用的に使えるはずです。

[前回は日報データの保存まで](https://qiita.com/goofmint/items/0615df8d9962e91a29c8)行いましたので、今回は日報データの検索処理を作成します。

## コードについて

今回のコードは[NCMBMania/swift\_daily\_report](https://github.com/NCMBMania/swift_daily_report)にアップロードしてあります。実装時の参考にしてください。

## ReportViewについて

日報の検索と一覧表示を行うのはReportViewになります。検索結果が入る `ary` はnullableとしていて、初期値はnilです。aryがnilの場合にはDatePickerだけを表示しています。

```swift
struct ReportView: View {
    @State private var date = Date()            // 検索対象の日付
    @State private var ary: [NCMBObject]? = nil // 検索結果のNCMBObject（配列）
    
    var body: some View {
        NavigationView {
            VStack(spacing: 10) {
                if self.ary == nil {
                    VStack {
                        // 検索対象を指定（日報の日付）
                        DatePicker("日付を選択してください",
                            selection: $date,
                            displayedComponents: .date
                        )
                        // 検索実行
                        Button(action: {
                            search()
                        }, label: {
                            Text("日報を表示")
                        })
                    }
                } else {
                    // 検索結果をリスト表示
                    List {
                        ForEach(self.ary!, id: \.objectId) { report in
                            ReportRowView(report: report)
                        }
                    }
                }
            }
            .navigationBarTitle("日報検索", displayMode: .inline)
            .toolbar {
                // 日付データを消す（最初の状態に戻す）アイコン
                ToolbarItem(placement: .navigationBarTrailing){
                    Button(action: {
                        self.ary = nil
                    }) {
                        Image(systemName: "xmark.circle")
                    }
                    .disabled(self.ary == nil)
                }
            }
        }
    }
    
    // 指定された日付の日報データを検索する関数
    func search() {
    }
}
```

![Simulator Screen Shot - iPod touch (7th generation) - 2021-10-06 at 15.19.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/b01eb538-837c-3a86-09b9-18dd0093df8c.png)


### 検索処理

検索処理は `query` 関数で行います。ここからの説明は `query` 関数の内容です。

まず検索用のオブジェクト、NCMBQueryを用意します。検索対象のクラス（DBでいうテーブル相当）はDailyReportを対象とします。

```swift
// 検索対象のデータストアのクラス
var query = NCMBQuery.getQuery(className: "DailyReport")
```

検索条件として日付を使います。例えば 2021/12/01 を指定された場合には `2021/12/01 00:00:00` <= date < `2021/12/02 00:00:00` といった条件で検索をします。以上検索はgreaterThanOrEqualTo、未満はlessThanになります。

```swift
// 日付用
let calendar = Calendar(identifier: .gregorian)
// 指定された日付の 00:00:00
let startAt = calendar.startOfDay(for: self.date)
// 指定された日付 +1 日
let endAt = calendar.date(byAdding: .day, value: 1, to: startAt)!
// 指定された日付の範囲（00:00:00以上〜次の日未満）を指定
query.where(field: "date", greaterThanOrEqualTo: startAt)
query.where(field: "date", lessThan: endAt)
```

これで検索を実行します。`find` は同期、 `findInBackground` は非同期検索になります。非同期検索の場合には `DispatchQueue.main.async` を使ってメインスレッドでデータを適用してください。

```swift
// 検索実行
query.findInBackground(callback: { result in
    // 処理が成功しているか判定
    if case let .success(ary) = result {
        // メインスレッドでデータを適用
        DispatchQueue.main.async {
            self.ary = ary
        }
    }
})
```

## 検索結果の表示

検索結果は配列になりますので、Listを使って描画します。

```swift
// 検索結果をリスト表示
List {
    ForEach(self.ary!, id: \.objectId) { report in
        ReportRowView(report: report)
    }
}
```

![Simulator Screen Shot - iPod touch (7th generation) - 2021-10-06 at 14.59.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/d2365071-2833-e3de-02ed-9c149e652fb7.png)


### ReportRowViewについて

検索結果の各行はReportRowViewにて表示します。検索結果画面から、reportとしてNCMBObjectを受け取ります。この時にはファイルストアの画像データはありませんので、最初はダミーの画像（写真アイコン）を表示しています。行が表示されたタイミングで loadImage を実行して、そこで画像を読み込みます。

```swift
struct ReportRowView: View {
    @State var report:NCMBObject // 表示する日報データ
    @State var imageData : Data = .init(capacity:0) // 日報に紐付いた画像データ
    
    var body: some View {
        // タップした際にの遷移
        NavigationLink(destination: DetailView(obj: report, imageData: $imageData)) {
            HStack(alignment: .top) {
                Spacer().frame(width: 10)
                VStack(alignment: .leading) {
                    HStack(alignment: .top) {
                        VStack {
                            // 画像データの有無を確認
                            if self.imageData.count == 0 {
                                // ないときにはダミー画像
                                Image(systemName: "photo")
                                    .resizable()
                                    .frame(width: 60, height: 60)
                                    .scaledToFit()
                            } else {
                                // 画像があれば表示
                                Image(uiImage: UIImage(data: self.imageData)!)
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                    .frame(width: 60, height: 60)
                            }
                        }
                        VStack(alignment: .leading) {
                            // 日報本文の一部を表示
                            Text((self.report["text"] ?? "") as! String)
                                .lineLimit(3)
                        }
                    }
                }
                Spacer()
            }
            .onAppear {
                // 画面が表示されたタイミングで画像を読み込む
                loadImage()
            }
        }
    }
    
    // 画像を読み込む関数
    func loadImage() {
    }
}
```

![ScreenShot_ 2021-10-06 15.26.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/ef111c01-f137-18d3-3097-46970915c98a.png)


### loadImageについて

loadImageは日報データにファイル名が指定されている場合、そのファイルを読み込む関数です。ここからの説明はloadImage関数内の内容です。まずファイル名を取得します。

```swift
// ファイル名
let fileName = self.report["fileName"] ?? ""
```

ファイル名が指定されているかどうか判定します。

```swift
// ファイル名がなければ終了
if fileName == "" {
    return
}
```

ファイル名があれば、そのファイル名を使ってNCMBFileを作成します。

```swift
// ファイルストアのオブジェクトを作成
let file = NCMBFile(fileName: fileName)
```

ファイル内容の取得は `fetchInBackground` を使います。同期的に行う `fetch` もありますが、ダウンロードサイズが大きいので処理が停止しない非同期メソッドの方がお勧めです。

処理がうまくいった場合については、検索の時と同じメインスレッドにて画像データを適用します。

```swift
// データをダウンロード
file.fetchInBackground(callback: { result in
    // 処理結果を判定
    if case let .success(data) = result {
        // うまくいっていればメインスレッドでデータを適用
        DispatchQueue.main.async {
            self.imageData = data!
        }
    }
})
```

## 日報データの詳細表示

検索結果の一覧にて日報データをタップした際には、詳細画面に遷移します。この時には、日報データとあらかじめ取得している画像データを渡します。画像データは重たいので、何度もダウンロードしていると動作が遅いと感じられてしまうでしょう。

```swift
// ReportRowViewより
NavigationLink(destination: DetailView(obj: report, imageData: $imageData)) {
}
```

DetailViewの内容です。こちらはNCMB側の処理はなく、受け取ったデータをシンプルに表示しているだけです。

```swift
// 日報の詳細表示用View
struct DetailView: View {
    @State var obj: NCMBObject    // 日報用データストアのオブジェクト
    @Binding var imageData : Data // 日報に添付されていた画像データ
    
    var body: some View {
        VStack {
            // 画像データがあれば表示
            if self.imageData.count > 0 {
                Image(uiImage: UIImage(data: self.imageData)!)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 200, height: 200)
            }
            // 日報の本文
            Text((obj["text"] ?? "") as! String)
        }
        // タイトルに日付を表示
        .navigationBarTitle(dateTitle(), displayMode: .inline)
    }
    
    // タイトルに出す内容を作成する
    func dateTitle() -> String {
        // 日付フォーマットの作成
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "ja_JP")
        dateFormatter.dateFormat = "M月d日の日報"
        // 日報データの日付を使ってタイトルを作成
        return dateFormatter.string(from: (obj["date"] ?? Date()) as! Date)
    }
}
```

![Simulator Screen Shot - iPod touch (7th generation) - 2021-10-06 at 14.59.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/10c2a8e4-84b4-ac01-c0c9-f040c0e838d7.png)


ここまでで日報アプリの完成です。

## まとめ

今回は認証を使っていませんが、利用すれば部署や社内での日報管理アプリにもできるでしょう。さらにコメント機能を追加することもできます。

データストアへの登録と検索、ファイルストアへのアップロードはどのようなアプリでも利用できる機能なので、ぜひ参考にしてアプリ開発に役立ててください。

[ドキュメント : 開発者向けドキュメント \| ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/#/Swift)