Добавьте функцию «Войти через Google» в приложение iOS

1. Прежде чем начать

В этой лабораторной работе вы научитесь создавать iOS-приложение, реализующее функцию «Вход через Google» и работающее в симуляторе. Реализации реализованы как на SwiftUI, так и на UIKit.

SwiftUI — это современный UI-фреймворк Apple для разработки новых приложений. Он позволяет создавать пользовательские интерфейсы для всех платформ Apple на основе единой кодовой базы. Требуется iOS версии не ниже 13.

UIKit — оригинальный и основополагающий UI-фреймворк Apple для iOS. Он обеспечивает обратную совместимость со старыми версиями iOS. Это делает его хорошим выбором для уже существующих приложений, которым требуется поддержка различных старых устройств.

Вы можете следовать по пути выбора фреймворка, который наилучшим образом соответствует вашим потребностям в разработке.

Предпосылки

Чему вы научитесь

  • Как создать проект Google Cloud
  • Как создать клиентов OAuth в Google Cloud Console
  • Как реализовать функцию «Войти через Google» в вашем приложении iOS
  • Как настроить кнопку «Войти через Google»
  • Как декодировать идентификационный токен
  • Как включить проверку приложений для вашего приложения iOS

Что вам понадобится

Эта лабораторная работа была создана в Xcode 16.3 с использованием симулятора iOS 18.3. Для разработки используйте последнюю версию Xcode.

2. Создайте новый проект Xcode.

  1. Откройте Xcode и выберите Создать новый проект Xcode .
  2. Выберите вкладку iOS , выберите шаблон приложения и нажмите Далее .

Страница шаблона создания проекта Xcode

  1. В вариантах проекта:
    • Введите название вашего продукта .
    • При желании выберите свою команду .
    • Введите идентификатор вашей организации .
    • Запишите сгенерированный идентификатор пакета . Он понадобится вам позже.
    • Для Интерфейса выберите один из вариантов:
      • SwiftUI для приложения на базе SwiftUI.
      • Раскадровка для приложения на базе UIKit.
    • Выберите Swift в качестве языка .
    • Нажмите «Далее» и выберите место для сохранения вашего проекта.

Страница параметров проекта Xcode

3. Создайте OAuth-клиент

Чтобы ваше приложение могло взаимодействовать со службами аутентификации Google, вам необходимо создать идентификатор клиента OAuth. Для этого потребуется проект Google Cloud. Следующие шаги помогут вам создать проект и идентификатор клиента OAuth.

Выберите или создайте проект Google Cloud

  1. Перейдите в Google Cloud Console и выберите или создайте проект. Если вы выбираете существующий проект, консоль автоматически перенаправит вас к следующему необходимому шагу.

Страница выбора проекта Google Cloud Console

  1. Введите имя для вашего нового проекта Google Cloud.
  2. Выберите Создать .

Страница выбора проекта Google Cloud Console

Если вы уже настроили экран согласия для выбранного проекта, вам не будет предложено настроить его сейчас. В этом случае вы можете пропустить этот раздел и перейти к разделу «Создание клиента OAuth 2.0» .

  1. Выберите Настроить экран согласия .

Google Cloud Console создает страницу клиента OAuth с настройкой требования экрана согласия

  1. Выберите « Начать» на странице брендинга.

Страница начала работы с брендингом Google Cloud Console

  1. На странице конфигурации проекта заполните следующие поля:
    • Информация о приложении : введите название и адрес электронной почты службы поддержки пользователей вашего приложения. Этот адрес будет доступен всем пользователям, чтобы они могли связаться с вами по вопросам, касающимся их согласия.
    • Аудитория : выберите Внешняя .
    • Контактная информация : введите адрес электронной почты, по которому Google сможет связаться с вами по поводу вашего проекта.
    • Ознакомьтесь с политикой использования пользовательских данных в разделе «Сервисы API Google».
    • Нажмите «Создать» .

Страница конфигурации брендинга клиента Google Cloud Console

  1. Выберите страницу «Клиенты» в навигационном меню.
  2. Нажмите «Создать клиента» .

Страница клиентов проекта Google CLoud

Создайте клиент OAuth 2.0

  1. Выберите iOS в качестве типа приложения .
  2. Введите имя вашего клиента.
  3. Введите идентификатор пакета, созданный на последнем шаге.
  4. Введите идентификатор команды , назначенный вашей команде Apple. Этот шаг пока необязателен, но он потребуется для включения проверки приложений в ходе выполнения этой практической работы.
  5. Выберите Создать .

Страница ввода данных клиента OAuth

  1. Скопируйте идентификатор клиента из диалогового окна, он понадобится вам позже.
  2. Загрузите файл plist для дальнейшего использования.

Диалог создания идентификатора клиента OAuth

4. Настройте свой проект Xcode

Следующий шаг — настроить проект Xcode для работы с SDK «Вход через Google». Этот процесс включает добавление SDK в ваш проект в качестве зависимости и настройку параметров проекта с использованием уникального идентификатора клиента. Этот идентификатор позволяет SDK безопасно взаимодействовать со службой аутентификации Google во время входа в систему.

Установите зависимости «Войти через Google»

  1. Откройте ваш проект Xcode.
  2. Перейдите в Файл > Добавить зависимости пакета .
  3. В строке поиска введите URL-адрес репозитория «Войти через Google»: https://github.com/google/GoogleSignIn-iOS

Найдите зависимость «Войти через Google» в диспетчере пакетов Swift

  1. Выберите Добавить пакет .
  2. Выберите основную цель приложения для пакета GoogleSignIn .
  3. Если вы используете SwiftUI, выберите основное целевое приложение для пакета GoogleSignInSwift . Если вы планируете использовать UIKit, не выбирайте целевое приложение для этого пакета.
  4. Выберите Добавить пакет .

Добавьте зависимость «Войти через Google» в свой проект

Настройте учетные данные вашего приложения

  1. В навигаторе проектов щелкните корень вашего проекта.
  2. В основной области редактора выберите основную цель приложения из списка ЦЕЛИ .
  3. Выберите вкладку «Информация» в верхней части области редактора.
  4. Наведите указатель мыши на последнюю строку в разделе «Свойства пользовательского объекта iOS» и нажмите появившуюся кнопку «+» .

Добавить новый ключ цели в свойства цели iOS

  1. В столбце «Ключ» введите GIDClientID.
  2. В столбце «Значение» вставьте идентификатор клиента, скопированный из Google Cloud Console.

Добавьте GIDClientID к основному целевому объекту приложения.

  1. Откройте файл plist , загруженный из Google Cloud Console.
  2. Скопируйте значение для обратного идентификатора клиента .

Файл plist консоли Google Cloud

  1. Разверните Типы URL в нижней части вкладки Информация .
  2. Нажмите кнопку + .
  3. Введите обратный идентификатор клиента в поле Схемы URL .

Добавить ключ URLSchemes к основному целевому приложению

Теперь мы готовы приступить к добавлению кнопки входа в наше приложение!

5. Добавьте кнопку входа.

После настройки проекта Xcode пришло время добавить в приложение кнопку «Войти через Google»!

Основная логика этого шага — вызов GIDSignIn.sharedInstance.signIn . Этот метод запускает процесс аутентификации, передавая управление SDK «Войти через Google» для отображения пользователю процесса входа через Google.

SwiftUI

  1. Найдите файл ContentView.swift в навигаторе проектов Xcode.
  2. Замените содержимое этого файла следующим текстом:
import GoogleSignIn
import GoogleSignInSwift
import SwiftUI

struct ContentView: View {
  var body: some View {
    VStack {
      GoogleSignInButton(action: handleSignInButton).padding()
    }
  }

  func handleSignInButton() {
    // Find the current window scene.
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
      print("There is no active window scene")
      return
    }

    // Get the root view controller from the window scene.
    guard
      let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
        .rootViewController
    else {
      print("There is no key window or root view controller")
      return
    }

    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(
      withPresenting: rootViewController
    ) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }
      // If sign in succeeded, display the app's main content View.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }
}

#Preview {
  ContentView()
}

Кнопка «Войти через Google» на платформе SwiftUI в симуляторе iOS

UIKit

  1. Найдите файл ViewController.swift в навигаторе проектов Xcode.
  2. Замените содержимое этого файла следующим текстом:
import GoogleSignIn
import UIKit

class ViewController: UIViewController {

  // Create an instance of the Sign in with Google button
  let signInButton = GIDSignInButton()

  override func viewDidLoad() {
    super.viewDidLoad()

    // Add the sign-in button to your view
    view.addSubview(signInButton)

    // Position the button using constraints
    signInButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])

    // Add a target to the button to call a method when it's pressed
    signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside)
  }

  // This method is called when the sign-in button is pressed.
  @objc func signInButtonTapped() {
    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      // If sign in succeeded, print the ID token.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }
}

Кнопка «Войти через Google» на платформе UIKit в симуляторе iOS

Просмотреть кнопку входа

Запустите приложение в симуляторе. Вы увидите кнопку «Войти через Google», но пока она не будет работать корректно. Это ожидаемо, так как вам ещё нужно реализовать код для обработки перенаправления обратно в приложение после аутентификации пользователя.

6. Настройте кнопку входа

Вы можете настроить кнопку «Войти через Google» по умолчанию, чтобы она лучше соответствовала теме вашего приложения. SDK «Войти через Google» позволяет изменить цветовую схему и стиль кнопки.

SwiftUI

Кнопка по умолчанию добавляется на страницу с помощью этой строки кода:

GoogleSignInButton(action: handleSignInButton)

Кнопка GoogleSignInButton настраивается путём передачи параметров её инициализатору. Следующий код добавит кнопке входа тёмную тему.

  1. Открыть ContentView.swift
  2. Обновите инициализатор GoogleSignInButton , добавив в него следующие значения:
GoogleSignInButton(
  scheme: .dark,  // Options: .light, .dark, .auto
  style: .standard,  // Options: .standard, .wide, .icon
  state: .normal,  // Options: .normal, .disabled
  action: handleSignInButton
).padding()

Темный режим SwiftUI Framework Кнопка «Войти через Google» на симуляторе iOS

Более подробную информацию о параметрах настройки см. в справочнике по фреймворку GoogleSignInSwift.

UIKit

Кнопка по умолчанию создается с помощью следующих строк кода:

// Create an instance of the Sign in with Google button
let signInButton = GIDSignInButton()

// Add the button to your view
view.addSubview(signInButton)

GIDSignInButton настраивается путём установки свойств экземпляра кнопки. Следующий код добавит кнопке входа тёмную тему.

  1. Откройте ViewController.swift .
  2. Добавьте следующие строки кода непосредственно перед добавлением кнопки входа в представление в функции viewDidLoad :
// Set the width and color of the sign-in button
signInButton.style = .standard  // Options: .standard, .wide, .iconOnly
signInButton.colorScheme = .dark  // Options: .dark, .light

Темный режим фреймворка UIKit Кнопка «Войти через Google» на симуляторе iOS

Более подробную информацию о настройке см. в справочнике по фреймворку GoogleSignIn.

7. Обработайте URL-адрес перенаправления аутентификации.

После добавления кнопки входа следующим шагом станет обработка перенаправления, которое происходит после аутентификации пользователя. После аутентификации Google возвращает URL с временным кодом авторизации. Для завершения процесса входа обработчик перехватывает этот URL и передает его в SDK «Войти через Google» для обмена на подписанный токен ID (JWT).

SwiftUI

  1. Откройте файл, содержащий структуру вашего App . Имя файла соответствует названию вашего проекта, поэтому оно будет выглядеть примерно так: YourProjectNameApp.swift .
  2. Замените содержимое этого файла следующим текстом:
import GoogleSignIn
import SwiftUI

@main
struct iOS_Sign_in_with_Google_App: App {
  var body: some Scene {
    WindowGroup {
      ContentView()

        .onOpenURL { url in
          GIDSignIn.sharedInstance.handle(url)
        }
    }
  }
}

UIKit

  1. Откройте AppDelegate.swift .
  2. Добавьте следующий импорт в начало файла:
import GoogleSignIn
  1. Добавьте следующую функцию-обработчик аутентификации в класс AppDelegate . Лучше всего разместить её сразу после закрывающей скобки метода application(_:didFinishLaunchingWithOptions:) :
func application(
  _ app: UIApplication,
  open url: URL,
  options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
  var handled: Bool

  handled = GIDSignIn.sharedInstance.handle(url)
  if handled {
    return true
  }
  // If not handled by this app, return false.
  return false
}

После внесения этих изменений ваш файл AppDelegate.swift должен выглядеть следующим образом:

import GoogleSignIn
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // Override point for customization after application launch.
    return true
  }

  func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
  ) -> Bool {
    var handled: Bool

    handled = GIDSignIn.sharedInstance.handle(url)
    if handled {
      return true
    }
    // If not handled by this app, return false.
    return false
  }

  // MARK: UISceneSession Lifecycle

  func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(
      name: "Default Configuration",
      sessionRole: connectingSceneSession.role
    )
  }

  func application(
    _ application: UIApplication,
    didDiscardSceneSessions sceneSessions: Set<UISceneSession>
  ) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
  }
}

Протестируйте процесс входа в систему

Теперь вы можете протестировать полный процесс входа в систему!

Запустите приложение и нажмите кнопку входа. После аутентификации Google откроет экран согласия, где вы сможете предоставить приложению доступ к вашим данным. После подтверждения вход будет завершён, и вы вернётесь в приложение.

После успешного входа SDK «Вход с Google» безопасно сохраняет учётные данные пользователя в связке ключей устройства. Эти данные можно использовать позже, чтобы пользователь оставался в системе при последующих запусках приложения.

8. Добавьте кнопку выхода

Теперь, когда вход работает, следующим шагом будет добавление кнопки выхода и обновление пользовательского интерфейса с учётом текущего состояния входа пользователя. При успешном входе SDK предоставляет объект GIDGoogleUser . Этот объект содержит свойство profile с базовой информацией, такой как имя пользователя и адрес электронной почты, которую вы будете использовать для персонализации пользовательского интерфейса.

SwiftUI

  1. Откройте файл ContentView.swift .
  2. Добавьте переменную состояния в начало структуры ContentView . Эта переменная будет хранить информацию о пользователе после входа в систему. Поскольку это переменная @State , SwiftUI будет автоматически обновлять ваш пользовательский интерфейс при каждом изменении её значения:
struct ContentView: View {
  @State private var user: GIDGoogleUser?
}
  1. Замените текущее body структуры ContentView следующим VStack . Это проверит, содержит ли переменная состояния user имя пользователя. Если да, будет показано приветственное сообщение и кнопка выхода. Если нет, будет отображена исходная кнопка «Войти через Google»:
var body: some View {
  VStack {
    // Check if the user is signed in.
    if let user = user {
      // If signed in, show a welcome message and the sign-out button.
      Text("Hello, \(user.profile?.givenName ?? "User")!")
        .font(.title)
        .padding()

      Button("Sign Out", action: signOut)
        .buttonStyle(.borderedProminent)

    } else {
      // If not signed in, show the "Sign in with Google" button.
      GoogleSignInButton(
        scheme: .dark,  // Options: .light, .dark, .auto
        style: .standard,  // Options: .standard, .wide, .icon
        state: .normal,  // Options: .normal, .disabled
        action: handleSignInButton
      ).padding()
    }
  }
}
  1. Обновите блок автодополнения handleSignInButton , чтобы присвоить signInResult.user новой переменной user . Это запустит переключение пользовательского интерфейса в режим входа в систему:
func handleSignInButton() {
  // Find the current window scene.
  guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
    print("There is no active window scene")
    return
  }

  // Get the root view controller from the window scene.
  guard
    let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
      .rootViewController
  else {
    print("There is no key window or root view controller")
    return
  }

  // Start the sign-in process.
  GIDSignIn.sharedInstance.signIn(
    withPresenting: rootViewController
  ) { signInResult, error in
    guard let result = signInResult else {
      // Inspect error
      print("Error signing in: \(error?.localizedDescription ?? "No error description")")
      return
    }

    DispatchQueue.main.async {
      self.user = result.user
    }

    // If sign in succeeded, display the app's main content View.
    print("ID Token: \(result.user.idToken?.tokenString ?? "")")
  }
}
  1. Добавьте новую функцию signOut в конец структуры ContentView , которая будет вызываться кнопкой выхода:
func signOut() {
  GIDSignIn.sharedInstance.signOut()
  // After signing out, set the `user` state variable to `nil`.
  self.user = nil
}

Запустите приложение и войдите в систему. После успешной аутентификации вы увидите изменение пользовательского интерфейса!

Состояние входа в фреймворк SwiftUI на симуляторе iOS

После внесения этих изменений ваш файл ContentView.swift должен выглядеть следующим образом:

import GoogleSignIn
import GoogleSignInSwift
import SwiftUI

struct ContentView: View {

  @State private var user: GIDGoogleUser?

  var body: some View {
    VStack {
      // Check if the user is signed in.
      if let user = user {
        // If signed in, show a welcome message and the sign-out button.
        Text("Hello, \(user.profile?.givenName ?? "User")!")
          .font(.title)
          .padding()

        Button("Sign Out", action: signOut)
          .buttonStyle(.borderedProminent)

      } else {
        // If not signed in, show the "Sign in with Google" button.
        GoogleSignInButton(
          scheme: .dark,  // Options: .light, .dark, .auto
          style: .standard,  // Options: .standard, .wide, .icon
          state: .normal,  // Options: .normal, .disabled
          action: handleSignInButton
        ).padding()
      }
    }
  }

  func handleSignInButton() {
    // Find the current window scene.
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
      print("There is no active window scene")
      return
    }

    // Get the root view controller from the window scene.
    guard
      let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
        .rootViewController
    else {
      print("There is no key window or root view controller")
      return
    }

    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(
      withPresenting: rootViewController
    ) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      DispatchQueue.main.async {
        self.user = result.user
      }

      // If sign in succeeded, display the app's main content View.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }

  func signOut() {
    GIDSignIn.sharedInstance.signOut()
    // After signing out, set the `user` state variable to `nil`.
    self.user = nil
  }
}

#Preview {
  ContentView()
}

UIKit

  1. Откройте ViewController.swift .
  2. В верхней части ViewController , прямо под тем местом, где вы объявили signInButton , добавьте кнопку выхода и приветственную надпись:
let signOutButton = UIButton(type: .system)
let welcomeLabel = UILabel()
  1. Добавьте следующую функцию в нижнюю часть ViewController . Эта функция будет отображать пользователю разный пользовательский интерфейс в зависимости от его статуса входа:
private func updateUI(for user: GIDGoogleUser?) {
  if let user = user {
    // User is signed in.
    signInButton.isHidden = true
    signOutButton.isHidden = false
    welcomeLabel.isHidden = false
    welcomeLabel.text = "Hello, \(user.profile?.givenName ?? "User")!"
  } else {
    // User is signed out.
    signInButton.isHidden = false
    signOutButton.isHidden = true
    welcomeLabel.isHidden = true
  }
}
  1. В нижней части функции viewDidLoad добавьте следующий код, чтобы добавить приветственную надпись и кнопку выхода в представление:
// --- Set up the Welcome Label ---
welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
welcomeLabel.textAlignment = .center
welcomeLabel.font = .systemFont(ofSize: 24, weight: .bold)
view.addSubview(welcomeLabel)

NSLayoutConstraint.activate([
  welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  welcomeLabel.bottomAnchor.constraint(equalTo: signInButton.topAnchor, constant: -20),
])

// --- Set up the Sign-Out Button ---
signOutButton.translatesAutoresizingMaskIntoConstraints = false
signOutButton.setTitle("Sign Out", for: .normal)
view.addSubview(signOutButton)

NSLayoutConstraint.activate([
  signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  signOutButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 20),
])

signOutButton.addTarget(self, action: #selector(signOutButtonTapped), for: .touchUpInside)

// --- Set Initial UI State ---
updateUI(for: nil)
  1. Обновите функцию signInButtonTapped для вызова метода UpdateUI при успешном входе в систему:
@objc func signInButtonTapped() {
  // Start the sign-in process.
  GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
    guard let result = signInResult else {
      // Inspect error
      print("Error signing in: \(error?.localizedDescription ?? "No error description")")
      return
    }

    // If sign in succeeded, print the ID token.
    print("ID Token: \(result.user.idToken?.tokenString ?? "")")

    DispatchQueue.main.async {
      self.updateUI(for: result.user)
    }
  }
}
  1. Наконец, добавьте функцию signOutButtonTapped в ViewController для управления процессом выхода из системы:
@objc func signOutButtonTapped() {
  GIDSignIn.sharedInstance.signOut()
  // Update the UI for the signed-out state.
  updateUI(for: nil)
}

Запустите приложение и войдите в систему. После успешной аутентификации вы увидите изменение пользовательского интерфейса!

Состояние входа в фреймворк UIKit на симуляторе iOS

После внесения этих изменений ваш файл ViewController.swift должен выглядеть следующим образом:

import GoogleSignIn
import UIKit

class ViewController: UIViewController {

  // Create an instance of the Sign in with Google button
  let signInButton = GIDSignInButton()
  let signOutButton = UIButton(type: .system)
  let welcomeLabel = UILabel()

  override func viewDidLoad() {
    super.viewDidLoad()

    // Set the width and color of the sign-in button
    signInButton.style = .standard  // Options: .standard, .wide, .iconOnly
    signInButton.colorScheme = .dark  // Options: .dark, .light

    // Add the sign-in button to your view
    view.addSubview(signInButton)

    // Position the button using constraints
    signInButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])

    // Add a target to the button to call a method when it's pressed
    signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside)

    // --- Set up the Welcome Label ---
    welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
    welcomeLabel.textAlignment = .center
    welcomeLabel.font = .systemFont(ofSize: 24, weight: .bold)
    view.addSubview(welcomeLabel)

    NSLayoutConstraint.activate([
      welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      welcomeLabel.bottomAnchor.constraint(equalTo: signInButton.topAnchor, constant: -20),
    ])

    // --- Set up the Sign-Out Button ---
    signOutButton.translatesAutoresizingMaskIntoConstraints = false
    signOutButton.setTitle("Sign Out", for: .normal)
    view.addSubview(signOutButton)

    NSLayoutConstraint.activate([
      signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signOutButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 20),
    ])

    signOutButton.addTarget(self, action: #selector(signOutButtonTapped), for: .touchUpInside)

    // --- Set Initial UI State ---
    updateUI(for: nil)
  }

  // This method is called when the sign-in button is pressed.
  @objc func signInButtonTapped() {
    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      // If sign in succeeded, print the ID token.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")

      DispatchQueue.main.async {
        self.updateUI(for: result.user)
      }
    }
  }

  private func updateUI(for user: GIDGoogleUser?) {
    if let user = user {
      // User is signed in.
      signInButton.isHidden = true
      signOutButton.isHidden = false
      welcomeLabel.isHidden = false
      welcomeLabel.text = "Hello, \(user.profile?.givenName ?? "User")!"
    } else {
      // User is signed out.
      signInButton.isHidden = false
      signOutButton.isHidden = true
      welcomeLabel.isHidden = true
    }
  }

  @objc func signOutButtonTapped() {
    GIDSignIn.sharedInstance.signOut()
    // Update the UI for the signed-out state.
    updateUI(for: nil)
  }
}

9. Восстановите состояние входа пользователя в систему

Чтобы улучшить взаимодействие с возвращающимися пользователями, следующим шагом будет восстановление состояния входа при запуске приложения. Вызов restorePreviousSignIn использует учётные данные, сохранённые в Keychain, для автоматического повторного входа пользователя, избавляя его от необходимости каждый раз проходить процедуру входа.

SwiftUI

  1. Откройте ContentView.swift .
  2. Добавьте следующий код непосредственно после VStack внутри переменной body :
.onAppear {
  // On appear, try to restore a previous sign-in.
  GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
    // This closure is called when the restoration is complete.
    if let user = user {
      // If a user was restored, update the `user` state variable.
      DispatchQueue.main.async {
        self.user = user
      }

      // Print the ID token to the console when restored.
      print("Restored ID Token: \(user.idToken?.tokenString ?? "")")
    }
  }
}

Ваш ContentView.swift должен выглядеть так:

import GoogleSignIn
import GoogleSignInSwift
import SwiftUI

struct ContentView: View {

  @State private var user: GIDGoogleUser?

  var body: some View {
    VStack {
      // Check if the user is signed in.
      if let user = user {
        // If signed in, show a welcome message and the sign-out button.
        Text("Hello, \(user.profile?.givenName ?? "User")!")
          .font(.title)
          .padding()

        Button("Sign Out", action: signOut)
          .buttonStyle(.borderedProminent)

      } else {
        // If not signed in, show the "Sign in with Google" button.
        GoogleSignInButton(
          scheme: .dark,  // Options: .light, .dark, .auto
          style: .standard,  // Options: .standard, .wide, .icon
          state: .normal,  // Options: .normal, .disabled
          action: handleSignInButton
        ).padding()
      }
    }

    .onAppear {
      // On appear, try to restore a previous sign-in.
      GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
        // This closure is called when the restoration is complete.
        if let user = user {
          // If a user was restored, update the `user` state variable.
          DispatchQueue.main.async {
            self.user = user
          }

          // Print the ID token to the console when restored.
          print("Restored ID Token: \(user.idToken?.tokenString ?? "")")
        }
      }
    }
  }

  func handleSignInButton() {
    // Find the current window scene.
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
      print("There is no active window scene")
      return
    }

    // Get the root view controller from the window scene.
    guard
      let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
        .rootViewController
    else {
      print("There is no key window or root view controller")
      return
    }

    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(
      withPresenting: rootViewController
    ) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      DispatchQueue.main.async {
        self.user = result.user
      }

      // If sign in succeeded, display the app's main content View.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }

  func signOut() {
    GIDSignIn.sharedInstance.signOut()
    // After signing out, set the `user` state variable to `nil`.
    self.user = nil
  }
}

#Preview {
  ContentView()
}

UIKit

  1. Откройте ViewController.swift .
  2. Добавьте следующий вызов restorePreviousSignIn в конец метода viewDidLoad :
// Attempt to restore a previous sign-in session
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
  if let user = user {
    print("Successfully restored sign-in for user: \(user.profile?.givenName ?? "Unknown")")

    // Print the ID token when a session is restored.
    print("Restored ID Token: \(user.idToken?.tokenString ?? "")")

    // On success, update the UI for the signed-in state on the main thread.
    DispatchQueue.main.async {
      self.updateUI(for: user)
    }
  }
}

Ваш файл ViewController.swift должен выглядеть так:

import GoogleSignIn
import UIKit

class ViewController: UIViewController {

  // Create an instance of the Sign in with Google button
  let signInButton = GIDSignInButton()
  let signOutButton = UIButton(type: .system)
  let welcomeLabel = UILabel()

  override func viewDidLoad() {
    super.viewDidLoad()

    // Set the width and color of the sign-in button
    signInButton.style = .standard  // Options: .standard, .wide, .iconOnly
    signInButton.colorScheme = .dark  // Options: .dark, .light

    // Add the sign-in button to your view
    view.addSubview(signInButton)

    // Position the button using constraints
    signInButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])

    // Add a target to the button to call a method when it's pressed
    signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside)

    // --- Set up the Welcome Label ---
    welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
    welcomeLabel.textAlignment = .center
    welcomeLabel.font = .systemFont(ofSize: 24, weight: .bold)
    view.addSubview(welcomeLabel)

    NSLayoutConstraint.activate([
      welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      welcomeLabel.bottomAnchor.constraint(equalTo: signInButton.topAnchor, constant: -20),
    ])

    // --- Set up the Sign-Out Button ---
    signOutButton.translatesAutoresizingMaskIntoConstraints = false
    signOutButton.setTitle("Sign Out", for: .normal)
    view.addSubview(signOutButton)

    NSLayoutConstraint.activate([
      signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signOutButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 20),
    ])

    signOutButton.addTarget(self, action: #selector(signOutButtonTapped), for: .touchUpInside)

    // --- Set Initial UI State ---
    updateUI(for: nil)

    // Attempt to restore a previous sign-in session
    GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
      if let user = user {
        print("Successfully restored sign-in for user: \(user.profile?.givenName ?? "Unknown")")

        // Print the ID token when a session is restored.
        print("Restored ID Token: \(user.idToken?.tokenString ?? "")")

        // On success, update the UI for the signed-in state on the main thread.
        DispatchQueue.main.async {
          self.updateUI(for: user)
        }
      }
    }
  }

  // This method is called when the sign-in button is pressed.
  @objc func signInButtonTapped() {
    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      // If sign in succeeded, print the ID token.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")

      DispatchQueue.main.async {
        self.updateUI(for: result.user)
      }
    }
  }

  private func updateUI(for user: GIDGoogleUser?) {
    if let user = user {
      // User is signed in.
      signInButton.isHidden = true
      signOutButton.isHidden = false
      welcomeLabel.isHidden = false
      welcomeLabel.text = "Hello, \(user.profile?.givenName ?? "User")!"
    } else {
      // User is signed out.
      signInButton.isHidden = false
      signOutButton.isHidden = true
      welcomeLabel.isHidden = true
    }
  }

  @objc func signOutButtonTapped() {
    GIDSignIn.sharedInstance.signOut()
    // Update the UI for the signed-out state.
    updateUI(for: nil)
  }
}

Протестируйте тихий вход

После входа полностью закройте приложение и запустите его снова. Вы увидите, что теперь вход выполнен автоматически, без необходимости нажимать кнопку.

10. Понимание идентификатора токена

Хотя объект GIDGoogleUser удобен для персонализации пользовательского интерфейса с использованием имени и адреса электронной почты пользователя, наиболее важным фрагментом данных, возвращаемым из SDK, является идентификационный токен.

В этой лабораторной работе используется онлайн-инструмент для проверки содержимого JWT. В рабочем приложении вам следует отправить этот ID-токен на ваш бэкенд-сервер. Ваш сервер должен проверить целостность ID-токена и использовать JWT для выполнения более значимых задач, например, для создания новой учётной записи на вашей бэкенд-платформе или для установки нового сеанса для пользователя.

Доступ к токену JWT и его декодирование

  1. Запустите приложение.
  2. Откройте консоль Xcode. Вы должны увидеть распечатанный идентификатор токена. Он будет выглядеть примерно так: eyJhbGciOiJSUzI1Ni ... Hecz6Wm4Q .
  3. Скопируйте идентификатор токена и используйте онлайн-инструмент, такой как jwt.io , для декодирования JWT.

Декодированный JWT будет выглядеть так:

{
  "alg": "RS256",
  "kid": "c8ab71530972bba20b49f78a09c9852c43ff9118",
  "typ": "JWT"
}
{
  "iss": "https://accounts.google.com",
  "azp": "171291171076-rrbkcjrp5jbte92ai9gub115ertscphi.apps.googleusercontent.com",
  "aud": "171291171076-rrbkcjrp5jbte92ai9gub115ertscphi.apps.googleusercontent.com",
  "sub": "10769150350006150715113082367",
  "email": "example@example.com",
  "email_verified": true,
  "at_hash": "JyCYDmHtzhjkb0-qJhKsMg",
  "name": "Kimya",
  "picture": "https://lh3.googleusercontent.com/a/ACg8ocIyy4VoR31t_n0biPVcScBHwZOCRaKVDb_MoaMYep65fyqoAw=s96-c",
  "given_name": "Kimya",
  "iat": 1758645896,
  "exp": 1758649496
}

Известные поля токенов

Декодированный идентификатор токена содержит поля с разными функциями. Некоторые из них, например, имя и адрес электронной почты, легко распознаются, другие используются вашим внутренним сервером для проверки.

Особенно важно понимать следующее поле:

  • sub : поле sub — это уникальный постоянный идентификатор учётной записи Google пользователя. Пользователь может изменить свой основной адрес электронной почты или имя, но его sub идентификатор останется неизменным. Это делает поле sub идеальным значением для использования в качестве первичного ключа для учётных записей внутренних пользователей.

Получить информацию о пользователе из идентификатора токена — более подробная информация о значении всех полей токена.

11. Защитите свое приложение с помощью App Check

Настоятельно рекомендуется включить функцию App Check, чтобы гарантировать, что доступ к конечным точкам Google OAuth 2.0 от имени вашего проекта будет иметь только ваше приложение. App Check проверяет, что запросы к вашим бэкэнд-сервисам исходят от вашего подлинного приложения на реальном и нетронутом устройстве.

В этом разделе показано, как интегрировать App Check в ваше приложение и настроить его как для отладки в симуляторе, так и для производственной сборки, работающей на реальном устройстве.

Настройка консоли

Для интеграции App Check в ваше приложение требуется однократная настройка в консолях Google Cloud и Firebase. Это включает в себя включение App Check для вашего OAuth-клиента iOS в консоли Google Cloud, создание ключа API для использования с поставщиком отладки App Check и привязку вашего проекта Google Cloud к Firebase.

Включить проверку приложений в Google Cloud Console

  1. Перейдите к списку клиентов , связанных с вашим проектом Google Cloud.
  2. Выберите идентификатор клиента OAuth 2.0, созданный для вашего приложения iOS.
  3. Включите проверку приложений в разделе «Идентификация Google» для iOS.

Страница редактирования клиента OAuth с переключателем проверки приложений

  1. Нажмите «Сохранить» .

Создать ключ API

  1. Перейдите на страницу библиотеки API для вашего проекта Google Cloud.
  2. Введите Firebase App Check API в строку поиска.

Страница библиотеки API Google Cloud Console

  1. Выберите и включите Firebase App Check API .
  2. Перейдите в раздел API и службы и выберите Учетные данные в навигационном меню.
  3. Выберите Создать учетные данные в верхней части страницы.

Страница учетных данных API Google Cloud Console

  1. Присвойте имя этому API-ключу.
  2. Выберите приложения iOS в разделе «Ограничения приложений» .
  3. Добавьте идентификатор пакета для вашего приложения как одобренного приложения.
  4. Выберите Ограничить ключ в разделе Ограничения API .
  5. В раскрывающемся меню выберите Firebase App Check API .
  6. Выберите Создать .

Страница создания ключа API Google Cloud Console

  1. Скопируйте созданный ключ API. Он понадобится вам на следующем этапе.

Добавьте Firebase в свой проект Google Cloud

  1. Перейдите в консоль Firebase .
  2. Выберите Начать с настройки проекта Firebase .
  3. Выберите Добавить Firebase в проект Google Cloud .

Добавить Firebase в существующий проект Google Cloud

  1. Выберите проект Google Cloud из раскрывающегося списка и продолжите процесс регистрации.
  2. Выберите Добавить Firebase .
  3. Когда ваш проект Firebase будет готов, нажмите Продолжить , чтобы открыть проект.

Интеграция клиентского кода

После настройки проекта Google Cloud для App Check пришло время написать клиентский код для его включения. Поставщик, используемый для аттестации, различается в рабочей и отладочной средах. Производственное приложение на реальном устройстве использует встроенный сервис Apple App Attest для подтверждения своей подлинности. Однако, поскольку симулятор iOS не может обеспечить такую ​​аттестацию, для отладочной среды требуется специальный поставщик отладки, которому передается ключ API.

Следующий код обрабатывает оба сценария, используя директиву компилятора для автоматического выбора правильного поставщика во время сборки.

SwiftUI

  1. Откройте основной файл приложения.
  2. Определите следующий класс AppDelegate после импорта и перед атрибутом @main :
class AppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {

    #if targetEnvironment(simulator)
      // Configure for debugging on a simulator.
      // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
      let apiKey = "YOUR_API_KEY"
      GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
        if let error {
          print("Error configuring GIDSignIn debug provider: \(error)")
        }
      }
    #else
      // Configure GIDSignIn for App Check on a real device.
      GIDSignIn.sharedInstance.configure { error in
        if let error {
          print("Error configuring GIDSignIn for App Check: \(error)")
        } else {
          print("GIDSignIn configured for App Check.")
        }
      }
    #endif

    return true
  }
}
  1. Замените "YOUR_API_KEY" в предоставленном коде на ключ API, скопированный из Google Cloud Console.
  2. Добавьте следующую строку в структуру App , прямо перед переменной body . Это зарегистрирует ваш класс AppDelegate в жизненном цикле приложения, что позволит ему реагировать на запуск приложения и другие системные события:
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

Ваш основной файл приложения должен выглядеть так:

import GoogleSignIn
import SwiftUI

class AppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {

    #if targetEnvironment(simulator)
      // Configure for debugging on a simulator.
      // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
      let apiKey = "YOUR_API_KEY"
      GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
        if let error {
          print("Error configuring GIDSignIn debug provider: \(error)")
        }
      }
    #else
      // Configure GIDSignIn for App Check on a real device.
      GIDSignIn.sharedInstance.configure { error in
        if let error {
          print("Error configuring GIDSignIn for App Check: \(error)")
        } else {
          print("GIDSignIn configured for App Check.")
        }
      }
    #endif

    return true
  }
}

@main
struct iOS_Sign_in_with_Google_App: App {

  @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

  var body: some Scene {
    WindowGroup {
      ContentView()

        .onOpenURL { url in
          GIDSignIn.sharedInstance.handle(url)
        }
    }
  }
}

UIKit

  1. Откройте AppDelegate.swift .
  2. Обновите метод application(_:didFinishLaunchingWithOptions:) , чтобы он содержал инициализацию App Check:
func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

  #if targetEnvironment(simulator)
    // Configure for debugging on a simulator.
    // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
    let apiKey = "YOUR_API_KEY"
    GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
      if let error {
        print("Error configuring GIDSignIn debug provider: \(error)")
      }
    }
  #else
    // Configure GIDSignIn for App Check on a real device.
    GIDSignIn.sharedInstance.configure { error in
      if let error {
        print("Error configuring GIDSignIn for App Check: \(error)")
      }
    }
  #endif

  return true
}
  1. Замените "YOUR_API_KEY" в предоставленном коде на ключ API, скопированный из Google Cloud Console.

Ваш файл AppDelegate.swift должен выглядеть так:

import GoogleSignIn
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    #if targetEnvironment(simulator)
      // Configure for debugging on a simulator.
      // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
      let apiKey = "YOUR_API_KEY"
      GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
        if let error {
          print("Error configuring GIDSignIn debug provider: \(error)")
        }
      }
    #else
      // Configure GIDSignIn for App Check on a real device.
      GIDSignIn.sharedInstance.configure { error in
        if let error {
          print("Error configuring GIDSignIn for App Check: \(error)")
        }
      }
    #endif

    return true
  }

  func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
  ) -> Bool {
    var handled: Bool

    handled = GIDSignIn.sharedInstance.handle(url)
    if handled {
      return true
    }
    // If not handled by this app, return false.
    return false
  }

  // MARK: UISceneSession Lifecycle

  func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(
      name: "Default Configuration",
      sessionRole: connectingSceneSession.role
    )
  }

  func application(
    _ application: UIApplication,
    didDiscardSceneSessions sceneSessions: Set<UISceneSession>
  ) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
  }
}

Тестовое приложение. Проверка на симуляторе.

  1. В строке меню Xcode выберите Продукт > Схема > Редактировать схему .
  2. В навигационном меню выберите пункт Выполнить .
  3. Выберите вкладку «Аргументы» .
  4. В разделе «Аргументы, передаваемые при запуске» выберите + и добавьте -FIRDebugEnabled . Этот аргумент запуска включает ведение журнала отладки Firebase.
  5. Выберите Закрыть .

Страница редактора аргументов Xcode

  1. Запустите свое приложение на симуляторе.
  2. Скопируйте токен отладки App Check , который выводится в консоли Xcode.

Отладочный токен App Check в консоли Xcode

  1. Перейдите к своему проекту в консоли Firebase .
  2. Разверните раздел «Сборка» в навигационном меню.
  3. Выберите «Проверка приложений» .
  4. Выберите вкладку «Приложения» .
  5. Наведите указатель мыши на свое приложение и выберите значок меню с тремя точками.

Настройки проверки приложений Firebase

  1. Выберите Управление токенами отладки .
  2. Выберите Добавить токен отладки .
  3. Дайте вашему токену отладки имя и вставьте скопированный ранее токен отладки в качестве значения.
  4. Нажмите «Сохранить» , чтобы зарегистрировать токен.

Управление отладочными токенами Firebase App Check

  1. Вернитесь в симулятор и войдите в систему.

Отображение метрик в консоли может занять несколько минут. После этого вы можете убедиться в работоспособности App Check, обратив внимание на увеличение количества проверенных запросов в одном из двух мест:

  • В разделе «Проверка приложений» консоли Firebase, на вкладке «API».

Метрики проверки приложений Firebase

  • На странице редактирования вашего клиента OAuth в Google Cloud Console.

Метрики проверки приложений Google Cloud Console

После мониторинга показателей App Check вашего приложения и подтверждения того, что легитимные запросы проверяются, следует включить принудительное применение App Check . После включения App Check отклоняет все непроверенные запросы, гарантируя, что доступ к конечным точкам OAuth 2.0 Google от имени вашего проекта будет получать только трафик из вашего подлинного приложения.

12. Дополнительные ресурсы

Поздравляю!

Вы настроили клиент OAuth 2.0 iOS, добавили кнопку «Войти через Google» в приложение iOS, узнали, как настраивать внешний вид кнопки, декодировали токен JWT ID и включили проверку приложений для своего приложения.

Эти ссылки могут помочь вам с дальнейшими шагами:

Часто задаваемые вопросы