💽

Googleフォームをバックエンドとして使う

2023/12/23に公開

これは株式会社TimeTree Advent Calendar 2023の23日目の記事です。

https://qiita.com/advent-calendar/2023/timetree

はじめに

こんにちは、TimeTree iOS エンジニアのNeganです。

モバイルアプリにおいて、ユーザーからのフィードバックや報告を受け付け、管理する機能は時として必要不可欠です。しかし、これを実現するために専用のAPIを開発することは、人材的にも時間的にもコストがかかる場合があります。そこで、私たちは、アプリから直接Googleフォームへデータを送信し、その結果をスプレッドシートで保存するというアプローチを採用しました。
タイトル通り、Googleフォームをバックエンドとして使うわけですが、意外にも手軽に導入することができましたので、簡単にですが紹介したいと思います。

今回は、アプリ内でユーザーからの意見を受け付ける機能を導入するという前提でお話しします。
ユーザーが名前と意見というデータをアプリから入力・送信し、それをGoogle Formを介してスプレッドシートに保存する仕組みを作成します。

画面を作る

それでは、まず、入力欄と投稿が可能なボタンを備えた意見入力画面を作成します。

Googleフォームを作る

次に、Googleフォームを作成しましょう。
その際、アプリ内で入力してもらう名前と意見の質問項目(記述式)を追加します。

追加後はGoogleFormのページをWebで表示をした後、Chromeの開発者ツール等を使いhtmlソースを確認します。

名前と意見の質問項目に該当する隠しフィールド(hidden field)を探しましょう。
今回の場合は下記となります。

<input type="hidden" name="entry.512733584" value="">
<input type="hidden" name="entry.1526336303" value="">

この隠しフィールドの name=entry.xxxxxxxxx こそがPOSTリクエストのリクエストパラメーター名となります。
また、生成したGoogleフォームのURLの末尾に /formResponse を追加すると、POSTする先のエンドポイントとして利用できます。

Googleフォームへリクエストする

これで投稿先のエンドポイントが作成できたので、後は、クライアント側で通常のREST APIと疎通する処理を書きましょう。

今回は例として以下のように記述します。

/// 送信ボタン
@MainActor private var submitButton: some View {
    Button {
        Task {
            do {
                let data = QuestionnaireData(name: inputName, opinion: inputOpinion)
                try await viewModel.repository.submit(data: data)
                await MainActor.run {
                    viewModel.eventFlow.send(.successSubmit)
                }
            } catch {
                await MainActor.run {
                    viewModel.eventFlow.send(.failedSubmit)
                }
            }
        }
    } label: {
        Text("送信")
    }.buttonStyle(SubmitButton())
        .padding(.bottom, 16)
}
// MARK: - QuestionnaireRepositoryProtocol

public protocol QuestionnaireRepositoryProtocol {
    /// データを送信する
    func submit(data: QuestionnaireData) async throws
}

@MainActor struct QuestionnaireRepository: QuestionnaireRepositoryProtocol {
    
    func submit(data: QuestionnaireData) async throws {
        try await GoogleForm.submitToForm(data: data)
    }
}
// Googleフォームへリクエストを行う処理
enum GoogleForm {
    
    private enum K {
        static let googleFormURL = "https://docs.google.com/forms/xxxxxxxxxxxxxxxxxx/formResponse"
    }
    /// 各GoogleFormで使うEntryIDをまとめた構造体
    struct EntryKeys {
        /// 名前
        let name: String
        /// 意見
        let opinion: String
        /// 意見提出用のキーの組み合わせ
        static let Feedback = EntryKeys(name:"entry.512733584" , opinion: "entry.1526336303")
    }
    
    /// 意見の送信
    /// - Parameters:
    ///   - name: 名前
    ///   - opinion: 意見
    static func submitToForm(data: QuestionnaireData) async throws {
        let keys = EntryKeys.Feedback
        let url = K.googleFormURL
        let parameters = [
            keys.name: data.name,
            keys.opinion: data.opinion
        ] as [String: Any]
        try await post(url: url, parameters: parameters)
    }
    
    /// フォームへの送信
    /// - Parameters:
    ///   - url: GoogleフォームのURL
    ///   - parameters: Googleフォームに送信するパラメーター
    private static func post(url: String, parameters: [String: Any]) async throws {
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: nil).validate(statusCode: 200 ..< 300)
                .responseString { response in
                    switch response.result {
                    case .success:
                        continuation.resume()
                    case let .failure(error):
                        continuation.resume(throwing: error)
                    }
                }
        }
    }
}

意見入力画面で、名前と意見を入力後に送信ボタンを押すと、無事、Googleフォームに紐づいたスプレッドシートにデータが保存されました。

嬉しいことにスプレッドシート側にはデフォルトでタイムスタンプのカラムが作成され、データが保存された際の時間が記録されています。

これならば時系列で投稿されたデータを管理することも可能ですね。便利。

まとめ

Googleフォーム、めちゃくちゃ便利だと思いませんか?
アプリ内でデータ収集を検討する際には、今回紹介したようにGoogleフォームをバックエンドとして利用してみるという選択肢も検討してみると良いかもしれません。

TimeTreeの採用情報

TimeTreeのミッションに向かって一緒に挑戦してくれる仲間を探しています。TimeTreeで働くことに興味がある方はぜひ、Company Deck(会社紹介資料)や採用ページをご覧ください!

https://docs.google.com/presentation/d/e/2PACX-1vQ2kFTDgn_hu0jFBuMw8qjIyiMFImX-c38lDyaDhPwXQwCCnGvBQIemMmb_FgF7Gl7Ga3MFEJBwES-1/pub?start=false&loop=false&delayms=3000&slide=id.p
https://timetreeapp.com/intl/ja/corporate/careers

Discussion