NCMBのSwift SDKを使ってカレンダーアプリを作る（その1：画面の説明とSDKの導入）


NCMBのSwift SDKを使ってカレンダーアプリを作ります。予定を登録したり、カレンダーコンポーネント（FSCalendar）を使って予定を表示できるというアプリです。

まず最初の記事では画面の説明とSDKの導入までを進めます。

## コードについて

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

## 利用技術について

今回は次のような組み合わせになっています。

- Swift 5.7.1
- [WenchaoD/FSCalendar: A fully customizable iOS calendar library, compatible with Objective-C and Swift](https://github.com/WenchaoD/FSCalendar)
- NCMB Swift SDK

## 仕様について

今回はNCMBのID/パスワード認証を利用しています。カレンダー表示についてはFSCalendarを利用し、アプリはSwiftUIで作成しています。

## 利用する機能について

カレンダーアプリで利用するNCMBの機能は次の通りです。

- 会員管理
	- ID/パスワード認証
- データストア
  - 予定の登録・更新・削除
  - 予定の検索

## 画面について

今回は以下の5つのViewがあります。

### ContentView

メインのViewです。認証状態によってログイン画面とカレンダー画面を出し分けています。

```swift
struct ContentView: View {
    @State var isLogin: Bool = NCMBUser.currentUser != nil
    var body: some View {
        // ログイン判定
        if isLogin {
            // ログイン済みの場合
            CalendarView();
        } else {
            // 未ログインの場合
            LoginView(isLogin: $isLogin);
        }
    }
}
```

### LoginView

![Simulator Screen Shot - iPhone 14 Pro - 2022-11-22 at 10.50.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/850120dd-d1f2-6c61-e073-923d4805b752.png)


認証を行うViewです。認証状態をContentViewにフィードバックしています。

```swift
struct LoginView: View {
    // 入力用
    @State private var userName: String = ""
    @State private var password: String = ""
    // レスポンス用
    @Binding var isLogin: Bool

    var body: some View {
        VStack(spacing: 16) {
            TextField("ユーザ名", text: $userName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: 280)
            SecureField("パスワード", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: 280)
            Button(action: {
                _signUpOrLogin()
            }, label: {
                Text("新規登録/ログイン")
            })
        }
    }
    
    // ユーザー登録またはログインを行う処理
    func _signUpOrLogin() -> Void {
			// 省略
    }
}
```

### CalendarView

![Simulator Screen Shot - iPhone 14 Pro - 2022-11-22 at 10.50.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/57faa254-532d-5155-2f43-bd2c4d811942.png)


カレンダー表示用のViewです。 `ObservableObject` を使ってカレンダーなどにデータを渡しています。

```swift
// カレンダー用の構造体
class DayData: ObservableObject {
    @Published var schedules: [NCMBObject] = []
    @Published var selectedDate = Date()
    @Published var currentYearMonth = Date()
}

struct CalendarView: View {
    @ObservedObject var dayData = DayData()
    // 作成・更新した際のフラグ
    @State private var updated = false
    // 削除されたオブジェクトIDを入れる
    @State private var deleteObjectId = ""
    // 作成したスケジュールが入るNCMBObject
    @State private var schedule: NCMBObject = NCMBObject(className: "Schedule")
    
    // 予定をNCMBのデータストアから取得する関数
    func _getSchedule() {
			// 省略
    }
    
    // 一覧のタイトル用
    func _viewTitle() -> String {
			// 省略
    }
    
    // 該当日のスケジュールだけを返す関数
    func _dateFilter() -> [NCMBObject] {
			// 省略
    }
    
    var body: some View {
        NavigationView {
            VStack {
                // カレンダービュー
                FSCalendarView(dayData: Binding.constant(dayData))
                    .frame(height: 400)
                    .onChange(of: dayData.currentYearMonth, perform: {(newValue) in
                        // 表示月が変わったら、予定を取得し直す
                        _getSchedule()
                    })
                Text(_viewTitle())
                    .font(.title)
                    .padding()
                // 選択した日の予定を一覧表示
                List(_dateFilter(), id: \.objectId) { schedule in
                    // 一覧をタップしたら、編集画面に遷移
                    NavigationLink(destination: FormView(schedule: Binding.constant(schedule), updated: $updated, deleteObjectId: $deleteObjectId)) {
                        // 一覧表示用
                        CalendarListItemView(schedule: schedule)
                    }
                }
                Spacer()
            }
            .navigationTitle("カレンダー")
            // ナビゲーションバーのプラスアイコン
            .navigationBarItems(trailing:
                                    NavigationLink(destination: FormView(schedule: $schedule, updated: $updated, deleteObjectId: $deleteObjectId), label: {
                    Image(systemName: "plus")
                })
            )
            // 予定を追加、更新した際のイベント
            .onChange(of: updated, perform: {_ in
                if updated {
                    // 予定を追加
                    if schedule.objectId != nil {
                        dayData.schedules.append(schedule)
                        schedule = NCMBObject(className: "Schedule")
                    }
                    // フラグを落とす
                    updated = false
                }
            })
            // 予定を削除された際のイベント
            .onChange(of: deleteObjectId, perform: {_ in
                if deleteObjectId != "" {
                    // 予定データから削除された予定を削除
                    dayData.schedules.removeAll(where: {
                        $0.objectId == deleteObjectId
                    })
                    // 削除されたオブジェクトIDをリセット
                    deleteObjectId = ""
                }
            })
        }
        // 表示された際にスケジュールを取得する
        .onAppear {
            _getSchedule()
        }
    }
}
```

### FSCalendarView

FSCalendarを操作するためのViewです。[【SwiftUI】FSCalendarの実装方法【イベント日表示と選択日表示の方法】](https://tomato-develop.com/swiftui-fscalendar-how-to-implement-show-event-selected-date/)を参考にさせてもらっています。

```swift
// カレンダービュー
struct FSCalendarView: UIViewRepresentable {
    @Binding var dayData: DayData
    func makeUIView(context: Context) -> FSCalendar {
        typealias UIViewType = FSCalendar
        let fsCalendar = FSCalendar()
        fsCalendar.delegate = context.coordinator
        fsCalendar.dataSource = context.coordinator
        fsCalendar.appearance.headerDateFormat = "yyyy年MM月"
        
        return fsCalendar
    }
    // 再描画用
    func updateUIView(_ uiView: FSCalendar, context: Context) {
        uiView.reloadData()
    }
    
    func makeCoordinator() -> Coordinator{
        return Coordinator(self)
    }
    
    class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource {
        var parent: FSCalendarView
        
        init(_ parent: FSCalendarView){
            self.parent = parent
        }
        
        // 予定がある日付にドットを表示する処理
        func calendar(_ calendar: FSCalendar, numberOfEventsFor date: Date) -> Int {
            // データがない場合は0を返して終わり
            if (parent.dayData.schedules.isEmpty) {
                return 0;
            }
            // 該当日のデータだけを抽出
            let events: [NCMBObject] = parent.dayData.schedules.filter({(obj) in
                if obj.objectId == nil {
                    return false
                }
                let startDate = obj["startDate"]! as Date
                let targetDate = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: startDate).date
                return date.compare(targetDate!) == .orderedSame
            })
            // 該当日のカウントを返す
            return events.count
        }
        
        // 日付を選択した際の処理
        func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
            parent.dayData.selectedDate = date
          
        }
        
        // 年月を変更した際の処理
        func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
            parent.dayData.currentYearMonth = calendar.currentPage
        }
        
    }
}
```

### CalendarListItemView

カレンダーで選択された日付にある予定を一覧表示する際に利用するViewです。CalendarListItemViewは各行あたりのViewを担当します。

```swift
// 予定の一覧用（行）
struct CalendarListItemView: View {
    @State var schedule: NCMBObject
    // 時間を表示する文字列を返す
    func _viewTime() -> String {
			// 省略
    }

    // 描画
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Text(schedule["title"]! as String)
                    .fontWeight(.bold)
                    .font(.title3)
                    .padding(.leading, 20)
                Spacer()
                Text(_viewTime())
                    .frame(alignment: .trailing)
                    .font(.caption)
                    .padding(.trailing, 20)
            }
            Text(schedule["body"]! as String)
                .padding(.horizontal, 20)
                .padding(.top, 5)
                .padding(.bottom, 10)
        }
        .frame(maxWidth: .infinity)
    }
}
```

### FormView

![Simulator Screen Shot - iPhone 14 Pro - 2022-11-22 at 10.50.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8ceb19aa-2f52-dd9e-785a-95535295ce72.png)


予定の入力・更新、削除を行うViewです。

```swift
struct FormView: View {
    // 一覧から受け取った予定
    @Binding var schedule: NCMBObject
    // 作成・更新用フラグ
    @Binding var updated: Bool
    // 削除用のオブジェクトIDを入れる
    @Binding var deleteObjectId: String
    
    @Environment(\.presentationMode) var presentation
    // 入力用
    @State private var _title: String = ""
    @State private var _body: String = ""
    @State private var _startDate: Date = Date.now
    @State private var _endDate: Date = Date.now
    
    // スケジュールデータを入力用に適用される処理
    func _setValue() -> Void {
			// 省略
    }
    
    // 開始日が変更されたら、それに合わせて終了日を自動設定
    func _setEndDate() -> Void {
			// 省略
    }
    
    // スケジュールの保存処理
    func _save() -> Void {
			// 省略
    }
    // 予定の削除処理
    func _delete() -> Void {
			// 省略
    }
    
    var body: some View {
        VStack(spacing: 16) {
            TextField("予定のタイトル", text: $_title)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: .infinity)
                .padding(.horizontal, 20)
            DatePicker("開始日時", selection: $_startDate)
                .padding(.horizontal, 20)
            DatePicker("終了日時", selection: $_endDate)
                .padding(.horizontal, 20)
            TextEditor(text: $_body)
                .frame(maxWidth: .infinity, maxHeight: 300)
                .border(.gray)
                .padding(.horizontal, 20)
            Button(action: {
                _save()
            }, label: {
                Text("新規保存 or 更新")
            })
            _schedule.wrappedValue.objectId != nil ?
            Button(action: {
                _delete()
            }, label: {
                Text("予定の削除")
            }) : nil
        // 開始日が変更された際のイベント
        }.onChange(of: _startDate, perform: {(_) in
            _setEndDate()
        })
        // 表示された際のイベント
        .onAppear {
            _setValue()
        }
    }
}
```

### SDKのインストール

今回はFSCalendarがCocoaPods向けにしか対応していないので、NCMB SDKについてもCocoaPods経由でインストールしています。Podfileの内容です。

```
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'calendar' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for calendar
  pod 'FSCalendar'
  pod 'NCMB', :git => 'https://github.com/NIFCLOUD-mbaas/ncmb_swift.git'
  target 'calendarTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'calendarUITests' do
    # Pods for testing
  end

end
```

後は `pod install` を実行してインストールは完了です。

## NCMBのAPIキーを取得

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)にてアプリを作成し、アプリケーションキーとクライアントキーを作成します。

## calendarApp.swiftの修正

〜App.swiftファイルを開いて、NCMB SDKの読み込みと初期化を行います。

```swift
import SwiftUI
import NCMB

@main
struct calendarApp: App {
    init() {
        // NCMBの初期化
        NCMB.initialize(applicationKey: "YOUR_APPLICATION_KEY", clientKey: "YOUR_CLIENT_KEY")
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

これでNCMBの初期化が完了します。

## まとめ

今回はカレンダーアプリの仕様と画面、NCMBの初期化までを解説しました。次は[ログイン処理を実装](https://qiita.com/goofmint/items/10ff0672cfe7d0501ef2)します。