Microsoft GraphAPIでドキュメントライブラリの画像を取得する方法(SwiftUI)

## 前置き＆概要

以下のようなドキュメントライブラリを作成した場合の画像をGraphAPIを使用して
SwiftUIで取得する方法を紹介します。
一回のGETで画像を取得できるわけではなく、さまざまなIDを取得したのちに画像を取得できます。
情報を探すのに苦労したので、まとめようと思いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616404/d0833dc8-42af-fbb7-6e04-5a8f3e48d201.png)

今回は他のSharePoint Listと関連させたかったため、"itemImageId"という列を追加し、
そのitemImageIdに紐づく画像を取得する方法を紹介します。
画像をGETしたiPhoneの画面は下記のようになります。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616404/947e736b-2dec-99f3-fabd-1c85a0873121.png">

画像を取得した際の動画は以下のようになります。
![Simulator Screen Recording - iPhone SE (3rd generation) - 2024-01-22 at 18.50.15.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616404/5982e737-efe7-9602-e3b9-9232d63f08ce.gif)


## 目次
* 環境、GraphAPI情報
    * 環境、Microsoftアカウント情報、使用パッケージ
    * 画像取得の所要時間
    * 必要なGraphAPIの権限
* 画像の取得
    * 画像の取得方法
    * コードサンプル
    * 注意点
* まとめ
* 引用、アイコン



## 環境、GraphAPI情報

* 環境、Microsoftアカウント情報、使用パッケージ

    * 環境
            □ MacBook Air
            □ macOS：Sonoma 14.2.1
            □ XCode：Version 15.0.1
     <br/>

    * Microsoftアカウント情報
            □  Microsoft 365 / Office 365 アカウント [職場または学校]
     <br/>

    * 使用パッケージ
            □ [MSAL](https://github.com/AzureAD/microsoft-authentication-library-for-objc)
            □ [Resolver](https://github.com/hmlongco/Resolver)
            □ （[SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)）
    <br/>
        
* 画像取得の所要時間
    **約 1.562 秒**（３回の平均。画像を取得するAPI１回の取得時間）
    画像の取得は時間を要するので、非同期で画像を格納し@Stateを使用して
    状態変数にデータを格納することを推奨します。
    時間の計測にはCFAbsoluteTimeGetCurrent()を使用しました。*引用参照
    <br/>
    

* 必要なGraphAPIの権限
    * "Sites.Read.All"：画像の名前を取得するため
    * "Files.Read"：ドキュメントライブラリを取得するため
       *いずれも「委任 (職場または学校のアカウント)」
       <br/>

## 画像の取得方法
GraphAPIで画像を取得するのは１回のGET操作で取得できます。
しかし、リクエストURLに必要な情報を取得するために複数のGET操作が必須です。
最終的に画像のGETに使用するリクエストURLは下記です。


    https://graph.microsoft.com/v1.0/sites/{site-id}/drives/{drive-id}/items/{画像ID}/content
    
上記の{drive-id}と{画像ID}を取得するために３回のGET操作をします。
site-idの取得方法や各種IDは[こちら](https://qiita.com/AikaTakatsu/items/5fd003e7b3fa22a1128e)の記事に詳細を記載しています。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616404/6bdbabb2-387c-ae11-727c-c92453e66c65.png)

該当のドキュメントライブラリ（ItemImage）は下記です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616404/996ba657-4711-2e57-f5d3-3183a3fc898e.png)

* コードサンプル

    * 画像をGETする関数の一部
    ```MsAuthAdapter.swift
		func getDriveImage(endPoint: String) async -> Result<UIImage, Stirng> {
		        var request = URLRequest(url: URL(string: "https://graph.microsoft.com" + endPoint)!)
		        Logger.msAuthentication.log("リクエスト: \(request)")
		        guard let accessToken = self.accessToken else { return .failure("noTokenError") }
		        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
		
		        // APIリクエストを送信
		        var (data, response): (Data, URLResponse)
		        do {
		            (data, response) = try await URLSession.shared.data(for: request)
		            //                レスポンスを全てプリント
		            if let responseDataString = String(data: data, encoding: .utf8) {
		                Logger.msAuthentication.log("Response Data: \(responseDataString)")
		            }
		        } catch {
		            // 原因不明エラー
		            return .failure("unknownError")
		        }
		
		        // リクエストエラー
		        guard let httpResponse = response as? HTTPURLResponse,
		            (200...299).contains(httpResponse.statusCode)
		        else {
		            Logger.msAuthentication.error("Request Error")
		            return .failure("unknownError")
		        }
		
		        // デコード処理
		        do {
		            // 結果をUIImageにして返す
		            guard let image = UIImage(data: data) else {
		                return .failure("jsonDecodeError")  // catchの役割
		            }
		            return .success(image)
		        }
		    }
    ```
    * 画像を取得するボタンと画像の表示
    ```View.swift
        Button("画像の取得") {
            let result = await msAuthAdapter.getDriveImage(
                    endPoint:
                    "/v1.0/sites/\(siteid)/drives/\(ItemImageDriveID)/items/\(ItemImageId)/content"
                    )
                    switch result {
                    // 結果をUIImageとして返している
                        case .success(let image):
                    // 非同期で画像を格納
                            DispatchQueue.main.async {
                                let newImageData = ShareImageDataModel(image: image)
                            self.sharepointData.imageDataModels.append(newImageData)
                            }
                        case .failure(let error):
                            print(error)
                    }
        }

        // 画像の表示
       
		if let imageData = sharepointData.imageDataModels.first?.image {
            Image(uiImage: imageData)
                .resizable()
                .scaledToFit()
                .frame(maxWidth: 100, maxHeight: 100, alignment: .center)
                .padding(3)
        } else {
            Image(systemName: "photo")
                .resizable()
                .scaledToFit()
                .frame(width: 100, height: 100)
                .padding(3)
            Text("no image")
                .foregroundColor(.gray)
        }
        
    ```

    * モデル
    ```Models.swift
		class SharepointDatas: ObservableObject {
			@Published var imageDataModels: [ShareImageDataModel] = []  // 画像を配列に格納
		}
		
		// Driveから取得した画像の格納
		struct ShareImageDataModel: Identifiable {
		    let id = UUID()  // 一意になるようにSwift側で自動生成
		    let image: UIImage?
		}

    ```

:::note warn
エラーハンドリングなどは記載していません。
:::


* 注意点
    * ドキュメントライブラリの名前を”英語”にしておく（Listとは違い、内部名ではなく表示名が使用される）
    * ドキュメントライブラリの”名前”列は一意の値が入るように運用する（既存の列であり、列の設定で一意の値に設定できないので、運用で補うしかない）GraphAPIで取得できる列がこの列のみ。


## まとめ
SwiftUI、SharePoint、Microsoft GraphAPIも使用し始めたばかりの初学者ですので、
他に良い方法や効率的な方法をご存知の方がいらっしゃればご教示いただけますと幸いです。


## 引用、アイコン、関連記事
* ドライバーのアイコン：https://icons8.jp/license
<a target="_blank" href="https://icons8.com/icon/TQP4bvS5pILT/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%A5%E3%83%BC%E3%83%89%E3%83%A9%E3%82%A4%E3%83%90%E3%83%BC">スクリュードライバー</a> アイコン by <a target="_blank" href="https://icons8.com">Icons8</a>

* [時間の計測](https://jp-seemore.com/app/15715/#toc7)

* GraphAPI関連
    * [リクエストURL一覧を参考](https://koruneko.hatenablog.com/entry/2022/12/19/034044)
    * [Driveに保存した画像のGET方法](https://www.youtube.com/watch?v=rjZnuuKQu3c)
    * [Microsoft GraphAPIを呼ぶ方法](https://learn.microsoft.com/en-us/entra/identity-platform/tutorial-v2-ios)

* 関連記事
    * Microsoft GraphAPIのID、概要、取得方法の一覧（作成中）

* コードの参考
    * 非同期
        * https://forums.swift.org/t/using-result-type-with-async-await/57003/10
        * https://peterfriese.dev/posts/swiftui-concurrency-essentials-part1/
        * https://blog.personal-factory.com/2022/01/23/how-to-use-async-await-since-swift5_5/
        * https://zenn.dev/akkyie/articles/swift-concurrency
        * https://software.small-desk.com/development/2020/10/17/swiftswiftui-result/
    * 画像の表示部分
        ChatGPT