前回、SwiftUIでGoogleアカウントにログインするためのログイン画面を用意しました。今回は実際にGoogleのサービスを利用してみます。まずはGoogle Driveを使います。
https://developers.google.com/identity/sign-in/ios/api-access?hl=ja
https://developers.google.com/drive/api/guides/about-sdk?hl=ja
Googleの公式ドキュメントです。
スコープの取得
使用するスコープについて
まず、アプリにGoogle Driveの使用ができるスコープが付与されているか確認します。Google Driveのフルドライブアクセスのスコープは以下の通りです。
https://www.googleapis.com/auth/drive
他にも、Google Driveで使用できるスコープは次のリファレンスに記述があります。
https://developers.google.com/drive/api/guides/api-specific-auth?hl=ja
今回は、Driveのフルアクセス権を利用しますが、一般公開するアプリを開発する場合には、必要最低限のアクセス権のみが付与されたスコープを使用する必要があります。
使用するAPIの有効化
これから使用するAPIをGoogle Cloud上で有効化します。
https://console.cloud.google.com/apis/library
OAuth クライアントIDを取得したGoogle アカウントで入り、使用するAPIを選択して有効化をしておきます。今回はGoogle Drive APIを利用するので、Google WorkplaceのカテゴリにあるGoogle Drive APIを選択して有効化してください。
スコープを取得するコードの記述
それでは、Google Driveのスコープが取得できるようなコードを作っていきましょう。
前回の記事より、GoogleアカウントにiOSからログインできるようになっているはずです。
import SwiftUI import GoogleSignIn import GoogleSignInSwift struct ContentView: View { @State var update = 0 func handleSignInButton() { guard let presentingviewcontroller = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController else {return} GIDSignIn.sharedInstance.signIn(withPresenting: presentingviewcontroller) { signInResult, error in guard signInResult != nil else { return } } } var body: some View { VStack { GoogleSignInButton(action: handleSignInButton) Button(action:{ GIDSignIn.sharedInstance.signOut() }){ HStack{ Image(systemName: "escape") .padding(.leading,10) .padding(.trailing,3) Text("ログアウト") } .bold() .font(.title3) .frame(width: 344,height: 40,alignment: .leading) .foregroundColor(.gray) .background(Color.white) .cornerRadius(3) .shadow(color: .gray,radius: 2,y:2) } } .padding() } }
そのため、ここに追加していく形でGoogle Driveへの対応を行います。
正常にGoogleアカウントにログインできたことを確認した後、スコープを取得する関数を実行するため、handleSignInButton関数に変更を加えます。
func handleSignInButton() { guard let presentingviewcontroller = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController else {return} GIDSignIn.sharedInstance.signIn(withPresenting: presentingviewcontroller) { signInResult, error in guard signInResult != nil else { return } guard let currentuser = GIDSignIn.sharedInstance.currentUser else { return } let scopes = ["https://googleapis.com/auth/drive"] currentuser.addScopes(scopes, presenting: presentingviewcontroller) { signInResult,error in guard error == nil else {return} guard signInResult != nil else {return} } } }
let scopesで、ユーザーに要求するスコープを設定し、次のaddScopesでユーザーにスコープを要求します。他にも要求したいスコープがある場合はscopesの配列に追加します。
実行
実際にアプリを実行すると、このような警告が表示される場合があります。
この警告は、Oauthでアプリが本番環境に設定されている場合、Googleの確認が終了していないことを意味しています。詳細ボタンから続行することができますが、本番環境からテストに戻しておくことをお勧めします。
うまくいけば、このようにアクセスのリクエストが表示されます。確認して許可しましょう。
Google Driveにアクセスする許可を得られたので、次に実際にファイルの操作を行ってみます。
SwiftでGoogle APIを扱えるようにする
まず、SwiftでGoogle APIを使用できるようにしましょう。Google API Client LibraryがGoogleから提供されているため、それを使用します。
https://github.com/google/google-api-objectivec-client-for-rest/
Xcodeのプロジェクトにパッケージを追加
パッケージを追加するプロジェクト上で、XcodeのツールバーからFile→Add Packages…をクリックします。
すると、このようなウィンドウが表示されます。ここでプロジェクトのパッケージを管理することができます。右上にリポジトリのURLを入力することで、簡単にパッケージを追加できます。今回は次のURLを使用します。
https://github.com/google/google-api-objectivec-client-for-rest.git
https://github.com/google/google-api-objectivec-client-for-rest/
でも問題ありません。
追加したいパッケージを選択して、右下のAdd Packageをクリックします。
この時、Google APIs…というタイトルの上にあるプロジェクトが正しく設定されているかどうかを確認しておいてください。
しばらく待つと、使用するAPIのパッケージを選択する画面に変わります。
とりあえず、以下のパッケージにチェックを入れて右下のAdd Packagesをクリックしましょう。
- GoogleAPIClientForREST_Drive
終了したら、モジュールをインポートしましょう。
import GoogleAPIClientForREST_Drive
ファイルの一覧表示
Google Driveの操作の一歩目として、Google Drive内のファイルをリスト表示するようなプログラムを作成してみましょう。
「リストを取得」ボタンの作成
まず、リストを取得するためのボタンを作成します。ボタンをタップすると、次に記述するリストを取得する関数を呼び出すようにします。
今の時点では、getList()を定義していないため、Xcode上に「Cannot find ‘getLists’ in scope」というエラーが表示されます。次の章でgetList()を適切に定義すれば、エラーは発生しなくなります。
Button(action:{ getList() }){ HStack { Image(systemName: "list.bullet") .padding(.leading,10) .padding(.trailing,3) Text("リストを取得") } .bold() .font(.title3) .frame(maxWidth: .infinity, alignment: .leading) .frame(height: 40) .foregroundColor(.gray) .background(Color.white) .cornerRadius(3) .shadow(color: .gray,radius: 2,y:2) }
前回作ったログアウトボタンを完全に踏襲しています。
検索結果を保存する配列の型の作成
まず、Google Driveから得られたレスポンスを配列として格納することができるように、新しい配列を作ります。Google Drive APIから送られてくる情報のうち、id,filename,kind.mimeTypeが格納できるようにしましょう。
struct File_List: Identifiable { var id:String = "" var filename:String = "" var kind:String = "" var mimeType:String = "" }
このコードは、
struct ContentView: View {...}
の前に記述しておきましょう。
Google Drive APIの処理 (getList関数の定義)
次に、Google Drive APIを使用して、ログインしているユーザーのGoogle Driveに保存されているすべてのファイルのリストを取得するプログラムを作りましょう。
@State var FileList = [File_List]() func getList() { let service = GTLRDriveService() let user = GIDSignIn.sharedInstance.currentUser guard let currentuser = user else {return} service.authorizer = currentuser.fetcherAuthorizer service.shouldFetchNextPages = true let query = GTLRDriveQuery_FilesList.query() query.q = "" service.executeQuery(query) { ticket,result,error in guard error == nil else { print(error!.localizedDescription) return } guard let result = result else {return} let filelist = result as! GTLRDrive_FileList //filelistの結果をリスト形式で取得 guard let files = filelist.files else {return} print(files) for i in files { FileList.append(File_List(id: i.identifier!, filename: i.name!, kind: i.kind!, mimeType: i.mimeType!)) } } }
まず、二行目の
let service = GTLRDriveService()
によって、Google Drive APIのクエリを実行するサービスを生成します。
次の行では、クエリを実行するGoogleアカウントを設定します。
GIDSignIn.sharedInstance.currentUser
から、現在ログインしているユーザーの情報を取り出すことができます。
guard let currentuser = user else {return} service.authorizer = currentuser.fetcherAuthorizer
ここでは、定義したユーザーが空でないことを確認した後、リクエストにユーザーの権限を付与します。
service.shouldFetchNextPages = true
一度のリクエストで全てのデータを要求します。ただし、処理に時間がよりかかかることになります。
let query = GTLRDriveQuery_FilesList.query()
Google Driveのファイルリストを取得するクエリを生成します。
query.q = ""
結果をフィルタリングします。何も指定しない場合、すべての結果が返されます。
qへの入力は次の公式リファレンスを参考にしてください。
https://developers.google.com/drive/api/guides/search-files?hl=ja#examples
例えば、abcという名前のファイルのみを出力する場合、以下のようにquery.qを設定します。
query.q = "name = 'abc'"
今回条件として設定しているものはqのみですが、その他のパラメータも設定することができます。
https://developers.google.com/drive/api/reference/rest/v3/files/list?hl=ja
例えば、上記のリファレンスのパラメータにあるcorporaを設定したい場合、次のようにqueryを記述します。
query.corpora = "allDrives"
service.executeQuery(query) { ticket,result,error in
この行で設定したクエリをGoogle Driveにリクエストします。
結果として、ticket、result、errorを返します。
guard error == nil else { print(error!.localizedDescription) return }
リクエストにエラーがあった場合、エラー内容を出力して終了します。
guard let result = result else {return} let filelist = result as! GTLRDrive_FileList
resultの内容がnilでないことを確認し、GTLRDrive_FileList型に変更します。
guard let files = filelist.files else {return} print(files) for i in files { FileList.append(File_List(id: i.identifier!, filename: i.name!, kind: i.kind!, mimeType: i.mimeType!)) }
最後に、先ほど定義したFile_List型の配列を使って一つずつリストに情報を格納していきます。
結果を表示するリストの作成
最後に、結果を表示するためのリストビューを作成しましょう。
List { ForEach(FileList) { item in VStack { Text(item.filename) HStack { Text(item.kind) Text(item.mimeType) } } } }
ここでは、ForEachを使ってFileListからひとつずつ要素を取り出し、Textで表示しています。
全体のコード
今まで書いてきたコードをあわせるとこのようになっているはずです。
import SwiftUI import GoogleSignIn import GoogleSignInSwift import GoogleAPIClientForREST_Drive struct File_List: Identifiable { var id:String = "" var filename:String = "" var kind:String = "" var mimeType:String = "" } struct ContentView: View { @State var update = 0 @State var accessToken:String = "" @State var FileList = [File_List]() func handleSignInButton() { guard let presentingviewcontroller = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController else {return} GIDSignIn.sharedInstance.signIn(withPresenting: presentingviewcontroller) { signInResult, error in update += 1 guard signInResult != nil else { return } guard let currentuser = GIDSignIn.sharedInstance.currentUser else { return } let scopes = ["https://www.googleapis.com/auth/drive"] currentuser.addScopes(scopes, presenting: presentingviewcontroller) { signInResult,error in guard error == nil else {return} guard signInResult != nil else {return} } } } func getList() { let service = GTLRDriveService() let user = GIDSignIn.sharedInstance.currentUser guard let currentuser = user else {return} service.authorizer = currentuser.fetcherAuthorizer service.shouldFetchNextPages = true let query = GTLRDriveQuery_FilesList.query() query.q = "" //リストの条件設定 service.executeQuery(query) { ticket,result,error in print(ticket) guard error == nil else { print(error!.localizedDescription) return } guard let result = result else {return} let filelist = result as! GTLRDrive_FileList //filelistの結果をリスト形式で取得 guard let files = filelist.files else {return} print(files) for i in files { FileList.append(File_List(id: i.identifier!, filename: i.name!, kind: i.kind!, mimeType: i.mimeType!)) } } } var body: some View { let _ = Self._printChanges() VStack { Text(GIDSignIn.sharedInstance.currentUser?.profile?.name ?? "") GoogleSignInButton(action: handleSignInButton) Button(action:{ GIDSignIn.sharedInstance.signOut() update += 1 }){ HStack{ Image(systemName: "escape") .padding(.leading,10) .padding(.trailing,3) Text("ログアウト") } .bold() .font(.title3) .frame(width: 344,height: 40,alignment: .leading) .foregroundColor(.gray) .background(Color.white) .cornerRadius(3) .shadow(color: .gray,radius: 2,y:2) } Button(action:{ getList() }){ HStack { Image(systemName: "list.bullet") .padding(.leading,10) .padding(.trailing,3) Text("リストを取得") } .bold() .font(.title3) .frame(maxWidth: .infinity, alignment: .leading) .frame(height: 40) .foregroundColor(.gray) .background(Color.white) .cornerRadius(3) .shadow(color: .gray,radius: 2,y:2) } if (UITraitCollection.current.userInterfaceStyle == .dark) { Text("\(update)") .foregroundColor(.black) } else { Text("\(update)") .foregroundColor(.white) } List { ForEach(FileList) { item in VStack { Text(item.filename) HStack { Text(item.kind) Text(item.mimeType) } } } } } .padding() } }
リストを取得ボタンをクリックすると、リストを表示するために自動で画面全体が更新されるため、update変数の設定は必要なくなります。
お疲れ様でした。