iOSアプリからOutlookの予定表を取得する方法(Swift)

## はじめに

Microsoft Graph APIを使って、iOSアプリからOutlookの予定表を取得する方法を紹介します。

## 「Microsoft Graph API」とは？

Microsoft 365のデータにアクセスするためのAPIです。

詳細は公式ドキュメントをご参照ください。
https://docs.microsoft.com/ja-jp/graph/overview

## 前提条件

- Microsoftアカウントを取得している
- アプリ登録の権限を持つAzure AD(Active Directory)が存在する  
Azure ADの追加方法は以下の通り  
https://blogs.technet.microsoft.com/jpazureid/2018/01/16/azuread-operation/

## 手順

公式ドキュメントに沿って実装します。
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/quickstart-v2-ios#option-2-register-and-manually-configure-your-application-and-code-sample

選択肢1(自動)はやり方がわからなかったので、今回は選択肢2(手動)で行います。

### アプリケーションの登録

まず、Azureにアプリを登録します。

以下のページにアクセスし、[+新規登録]をクリックします。
https://aka.ms/MobileAppReg
<img width="993" alt="スクリーンショット_2019-06-24_22_17_59.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/ca337fad-98c1-5c1c-f85a-caf66f5b7f51.jpeg">

名前を入力し、[登録]をクリックします。
<img width="971" alt="スクリーンショット_2019-06-24_22_24_23.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/3cf16a1c-ab9c-ba6b-9587-f8dad39c59dc.jpeg">

[サポートされているアカウントの種類]では、「この組織のディレクトリ内のアカウントのみ」を選択するとシングルテナントアプリ、他の2つを選択するとマルチテナントアプリとなります。

これでアプリの登録は完了です。

### Bundle IDの更新

次に、登録したアプリにBundle IDを追加します。
※公式ドキュメントの手順がわからなかったので、異なる方法で行っています。

[クイック スタート] > [iOS]をクリックします。
<img width="1280" alt="スクリーンショット_2019-06-24_22_57_34.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/cacfa4d8-7b03-b983-cd73-309f5ea431b1.jpeg">

[Step 1: Configure your application]にある[Make this change for me]をクリックします。
<img width="620" alt="スクリーンショット 2019-06-24 23.01.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/1cf4493f-f9f5-5ebd-3ec7-250f08d60843.png">

[バンドル ID]にiOSアプリのBundle Identifierを入力し、[更新する]をクリックします。
<img width="588" alt="スクリーンショット_2019-06-24_23_02_32.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/309788be-9c9b-7205-7d9f-310667507056.jpeg">

後述するサンプルコードのBundle IDは `com.microsoft.identitysample.MSALiOS` です。
<img width="735" alt="スクリーンショット_2019-06-24_23_02_59.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/09222095-73fc-66ca-2bab-2f5c66712cdd.jpeg">

更新が完了したら緑のチェックが付きます。
<img width="735" alt="スクリーンショット 2019-06-24 23.04.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/a60f623c-2675-d814-2f56-ed5def15e2cb.png">

※一度Bundle IDを変更すると、[Make this change for me]が非表示になるため、再度Bundle IDを変更できなくなります。
変更する方法を知っている方がいらっしゃったら教えてください:bow:

こちらのページの `kClientID` と `kAuthority` は後で使うので、ページは開いたままにしてください。

### 実装

#### サンプルコードのダウンロード

以下からサンプルコードをダウンロードします。
https://github.com/Azure-Samples/active-directory-ios-swift-native-v2/archive/master.zip

#### クライアントIDと認証URLの更新

MSALiOS.xcworkspaceをXcodeで開きます。

クライアントIDと認証URLを、先ほど開いたページに記述されている値に変更します。

```diff_swift:ViewController.swift
- let kClientID = "{デフォルトのクライアントID}"
+ let kClientID = "{自アプリのクライアントID}"
…
let kAuthority = "https://login.microsoftonline.com/common" // マルチテナントアプリ
let kAuthority = "https://login.microsoftonline.com/{自アプリのディレクトリ(テナント)ID}" // シングルテナントアプリ
```

`kAuthority` はマルチテナントアプリとシングルテナントアプリで指定する値が異なります。

マルチテナントやシングルテナントについては、公式ドキュメントをご参照ください。
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant

#### 権限の追加

必要に応じて権限を追加します。
私は予定表を取得したいので、 `Calendars.Read` を追加しました。

予定表の取得に必要な権限は、以下に記載されています。
https://docs.microsoft.com/ja-jp/graph/api/user-list-calendarview?view=graph-rest-1.0&tabs=cs

```swift:ViewController.swift
let kScopes: [String] = ["https://graph.microsoft.com/User.Read",
                         "https://graph.microsoft.com/Calendars.Read"]
```

#### URLスキームの変更

Info.plistに記述されているURLスキームを以下のように書き換えます。
サンプルアプリだとこのままでも動作しますが、書き換えておくのがいいです。

```diff_xml:Info.plist
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
-           <string>msauth.com.microsoft.identitysample.MSALiOS</string>
+           <string>msauth.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        </array>
    </dict>
</array>
```

### デバッグ

ここまでできたらデバッグして動作確認します。

[Call Microsoft Graph API]をタップします。
<img width="329" alt="スクリーンショット_2019-06-24_23_37_50.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/4d03ee72-4490-b4b0-f182-63d2b74f4cbd.jpeg">

[Continue]をタップします。
<img width="329" alt="スクリーンショット_2019-06-24_23_39_26.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/97b9b065-abc4-432f-6970-31c14aa70155.jpeg">

Microsoftアカウントを入力し、[Next]をタップします。
<img width="329" alt="スクリーンショット_2019-06-24_23_41_28.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/3443c6c4-1c94-1eb9-eca8-16b666f8453a.jpeg">

パスワードを入力し、[Sign in]をタップします。
<img width="329" alt="スクリーンショット_2019-06-24_23_43_20.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/2abfe377-62e9-0bab-5201-58889ffa3bcb.jpeg">

[Result from Graph:]にJSONデータが表示されたらログイン成功です！
<img width="329" alt="スクリーンショット_2019-06-24_23_45_04.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/cee88a4b-41a1-d0b8-aed8-29b4839d4920.jpeg">

### 予定表の取得

自分の1週間の予定表を取得する処理を実装します。

GraphエクスプローラーにサインインしてAPIを試し、クエリやレスポンスボディを把握すると実装しやすいです。
https://developer.microsoft.com/ja-jp/graph/graph-explorer

サンプルコードの `getContentWithToken()` メソッドをコピぺし、メソッド名とURLのみ変更すればOKです。

```swift:ViewController.swift
    private func getCalendarWithToken() {
        let formatter = DateFormatter.fullISO8601
        let startDateTime = formatter.string(from: Date())
        let endDateTime = formatter.string(from: Date(timeIntervalSinceNow: 60*60*24*7))
        
        let urlString = kGraphURI + "calendarview?startdatetime=" + startDateTime + "&enddatetime=" + endDateTime
        
        // Specify the Graph API endpoint
        let url = URL(string: urlString)
        var request = URLRequest(url: url!)
        
        // Set the Authorization header for the request. We use Bearer tokens, so we specify Bearer + the token we got from the result
        request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization")
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                self.updateLogging(text: "Couldn't get graph result: \(error)")
                return
            }
            
            guard let result = try? JSONSerialization.jsonObject(with: data!, options: []) else {
                self.updateLogging(text: "Couldn't deserialize result JSON")
                return
            }
            
            self.updateLogging(text: "Result from Graph: \(result))")
            }.resume()
    }

}
```

```swift:DateFormatter+ISO8601.swift
import Foundation

extension DateFormatter {
    static let fullISO8601: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter
    }()
}
```

`getContentWithToken()` メソッドを差し替え、デバッグします。(2箇所)

```diff_swift:ViewController.swift
- self.getContentWithToken()
+ self.getCalendarWithToken()
```

予定表のデータがJSON形式で表示されたら成功です！
<img width="329" alt="スクリーンショット_2019-06-25_0_32_06.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/3e1d0bae-1a58-e96f-95d1-0aeda17aa326.jpeg">

## おわりに

あとはこれらのコードを参考に製品アプリを実装すればOKです。

iOSアプリからMicrosoftのデータを取得するのはなかなか大変でした:sweat_smile:

## 参考リンク

- [【Swift】型を使うという意味を考える (Codable再入門) - Qiita](https://qiita.com/shiz/items/f37b489e9f41678932f0#dateencodingstrategy--datedecodingstrategy)  
ISO 8601フォーマットの日付を文字列に変換する方法を参考にさせていただきました。