Skip to content

Commit

Permalink
Use async await API in DaysUntilBirthday Sample App
Browse files Browse the repository at this point in the history
  • Loading branch information
mdmathias committed Aug 5, 2022
1 parent e46e0cb commit db1677e
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 197 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import Combine
import GoogleSignIn

/// An observable class to load the current user's birthday.
final class BirthdayLoader: ObservableObject {
/// A class to load the current user's birthday.
final class BirthdayLoader {
/// The scope required to read a user's birthday.
static let birthdayReadScope = "https://www.googleapis.com/auth/user.birthday.read"
private let baseUrlString = "https://people.googleapis.com/v1/people/me"
Expand Down Expand Up @@ -51,62 +51,49 @@ final class BirthdayLoader: ObservableObject {
return URLSession(configuration: configuration)
}()

private func sessionWithFreshToken(completion: @escaping (Result<URLSession, Error>) -> Void) {
let authentication = GIDSignIn.sharedInstance.currentUser?.authentication
authentication?.do { auth, error in
guard let token = auth?.accessToken else {
completion(.failure(.couldNotCreateURLSession(error)))
return
}
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Authorization": "Bearer \(token)"
]
let session = URLSession(configuration: configuration)
completion(.success(session))
private func sessionWithFreshToken() async throws -> URLSession {
guard let authentication = GIDSignIn.sharedInstance.currentUser?.authentication else {
throw Error.noCurrentUserForSessionWithFreshToken
}

let freshAuth = try await authentication.doWithFreshTokens()
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Authorization": "Bearer \(freshAuth.accessToken)"
]
let session = URLSession(configuration: configuration)
return session
}

/// Creates a `Publisher` to fetch a user's `Birthday`.
/// - parameter completion: A closure passing back the `AnyPublisher<Birthday, Error>`
/// upon success.
/// - note: The `AnyPublisher` passed back through the `completion` closure is created with a
/// fresh token. See `sessionWithFreshToken(completion:)` for more details.
func birthdayPublisher(completion: @escaping (AnyPublisher<Birthday, Error>) -> Void) {
sessionWithFreshToken { [weak self] result in
switch result {
case .success(let authSession):
guard let request = self?.request else {
return completion(Fail(error: .couldNotCreateURLRequest).eraseToAnyPublisher())
/// Fetches a `Birthday`.
/// - returns An instance of `Birthday`.
/// - throws: An instance of `BirthdayLoader.Error` arising while fetching a birthday.
func loadBirthday() async throws -> Birthday {
let session = try await sessionWithFreshToken()
guard let request = request else {
throw Error.couldNotCreateURLRequest
}
let birthdayData = try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Data, Swift.Error>) -> Void in
let task = session.dataTask(with: request) { data, response, error in
guard let data = data else {
return continuation.resume(throwing: error ?? Error.noBirthdayData)
}
let bdayPublisher = authSession.dataTaskPublisher(for: request)
.tryMap { data, error -> Birthday in
let decoder = JSONDecoder()
let birthdayResponse = try decoder.decode(BirthdayResponse.self, from: data)
return birthdayResponse.firstBirthday
}
.mapError { error -> Error in
guard let loaderError = error as? Error else {
return Error.couldNotFetchBirthday(underlying: error)
}
return loaderError
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
completion(bdayPublisher)
case .failure(let error):
completion(Fail(error: error).eraseToAnyPublisher())
continuation.resume(returning: data)
}
task.resume()
}
let decoder = JSONDecoder()
let birthdayResponse = try decoder.decode(BirthdayResponse.self, from: birthdayData)
return birthdayResponse.firstBirthday
}
}

extension BirthdayLoader {
/// An error representing what went wrong in fetching a user's number of day until their birthday.
/// An error for what went wrong in fetching a user's number of days until their birthday.
enum Error: Swift.Error {
case couldNotCreateURLSession(Swift.Error?)
case noCurrentUserForSessionWithFreshToken
case couldNotCreateURLRequest
case userHasNoBirthday
case couldNotFetchBirthday(underlying: Swift.Error)
case noBirthdayData
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@

import Foundation
import GoogleSignIn
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif

/// An observable class for authenticating via Google.
final class GoogleSignInAuthenticator: ObservableObject {
final class GoogleSignInAuthenticator {
// TODO: Replace this with your own ID.
#if os(iOS)
private let clientID = "687389107077-8qr6dh8fr4uaja89sdr5ieqb7mep04qv.apps.googleusercontent.com"
Expand All @@ -38,40 +43,32 @@ final class GoogleSignInAuthenticator: ObservableObject {
self.authViewModel = authViewModel
}

/// Signs in the user based upon the selected account.'
/// - note: Successful calls to this will set the `authViewModel`'s `state` property.
func signIn() {
#if os(iOS)
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
print("There is no root view controller!")
return
}

GIDSignIn.sharedInstance.signIn(with: configuration,
presenting: rootViewController) { user, error in
guard let user = user else {
print("Error! \(String(describing: error))")
return
}
self.authViewModel.state = .signedIn(user)
}

#elseif os(macOS)
guard let presentingWindow = NSApplication.shared.windows.first else {
print("There is no presenting window!")
return
}
#if os(iOS)
/// Signs in the user based upon the selected account.
/// - parameter rootViewController: The `UIViewController` to use during the sign in flow.
/// - returns: The signed in `GIDGoogleUser`.
/// - throws: Any error that may arise during the sign in process.
func signIn(with rootViewController: UIViewController) async throws -> GIDGoogleUser {
return try await GIDSignIn.sharedInstance.signIn(
with: configuration,
presenting: rootViewController
)
}
#endif

GIDSignIn.sharedInstance.signIn(with: configuration,
presenting: presentingWindow) { user, error in
guard let user = user else {
print("Error! \(String(describing: error))")
return
}
self.authViewModel.state = .signedIn(user)
}
#endif
#if os(macOS)
/// Signs in the user based upon the selected account.
/// - parameter window: The `NSWindow` to use during the sign in flow.
/// - returns: The signed in `GIDGoogleUser`.
/// - throws: Any error that may arise during the sign in process.
func signIn(with window: NSWindow) async throws -> GIDGoogleUser {
return try await GIDSignIn.sharedInstance.signIn(
with: configuration,
presenting: window
)
}
#endif

/// Signs out the current user.
func signOut() {
Expand All @@ -80,57 +77,41 @@ final class GoogleSignInAuthenticator: ObservableObject {
}

/// Disconnects the previously granted scope and signs the user out.
func disconnect() {
GIDSignIn.sharedInstance.disconnect { error in
if let error = error {
print("Encountered error disconnecting scope: \(error).")
}
self.signOut()
}
func disconnect() async throws {
try await GIDSignIn.sharedInstance.disconnect()
}

// Confines birthday calucation to iOS for now.
#if os(iOS)
/// Adds the birthday read scope for the current user.
/// - parameter completion: An escaping closure that is called upon successful completion of the
/// `addScopes(_:presenting:)` request.
/// - note: Successful requests will update the `authViewModel.state` with a new current user that
/// has the granted scope.
func addBirthdayReadScope(completion: @escaping () -> Void) {
#if os(iOS)
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
fatalError("No root view controller!")
}

GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope],
presenting: rootViewController) { user, error in
if let error = error {
print("Found error while adding birthday read scope: \(error).")
return
}

guard let currentUser = user else { return }
self.authViewModel.state = .signedIn(currentUser)
completion()
}

#elseif os(macOS)
guard let presentingWindow = NSApplication.shared.windows.first else {
fatalError("No presenting window!")
}

GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope],
presenting: presentingWindow) { user, error in
if let error = error {
print("Found error while adding birthday read scope: \(error).")
return
}

guard let currentUser = user else { return }
self.authViewModel.state = .signedIn(currentUser)
completion()
}
/// - parameter viewController: The `UIViewController` to use while authorizing the scope.
/// - returns: The `GIDGoogleUser` with the authorized scope.
/// - throws: Any error that may arise while authorizing the scope.
func addBirthdayReadScope(viewController: UIViewController) async throws -> GIDGoogleUser {
return try await GIDSignIn.sharedInstance.addScopes(
[BirthdayLoader.birthdayReadScope],
presenting: viewController
)
}
#endif

#endif
#if os(macOS)
/// Adds the birthday read scope for the current user.
/// - parameter window: The `NSWindow` to use while authorizing the scope.
/// - returns: The `GIDGoogleUser` with the authorized scope.
/// - throws: Any error that may arise while authorizing the scope.
func addBirthdayReadScope(window: NSWindow) async throws -> GIDGoogleUser {
return try await GIDSignIn.sharedInstance.addScopes(
[BirthdayLoader.birthdayReadScope],
presenting: window
)
}
#endif
}

extension GoogleSignInAuthenticator {
enum Error: Swift.Error {
case failedToSignIn
case failedToAddBirthdayReadScope(Swift.Error)
case userUnexpectedlyNilWhileAddingBirthdayReadScope
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,35 @@ final class AuthenticationViewModel: ObservableObject {

/// Signs the user in.
func signIn() {
authenticator.signIn()
#if os(iOS)
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
print("There is no root view controller!")
return
}

Task { @MainActor in
do {
let user = try await authenticator.signIn(with: rootViewController)
self.state = .signedIn(user)
} catch {
print("Error signing in: \(error)")
}
}
#elseif os(macOS)
guard let presentingWindow = NSApplication.shared.windows.first else {
print("There is no presenting window!")
return
}

Task { @MainActor in
do {
let user = try await authenticator.signIn(with: presentingWindow)
self.state = .signedIn(user)
} catch {
print("Error signing in: \(error)")
}
}
#endif
}

/// Signs the user out.
Expand All @@ -57,19 +85,39 @@ final class AuthenticationViewModel: ObservableObject {

/// Disconnects the previously granted scope and logs the user out.
func disconnect() {
authenticator.disconnect()
Task { @MainActor in
do {
try await authenticator.disconnect()
authenticator.signOut()
} catch {
print("Error disconnecting: \(error)")
}
}
}

var hasBirthdayReadScope: Bool {
return authorizedScopes.contains(BirthdayLoader.birthdayReadScope)
}

#if os(iOS)
/// Adds the requested birthday read scope.
/// - parameter completion: An escaping closure that is called upon successful completion.
func addBirthdayReadScope(completion: @escaping () -> Void) {
authenticator.addBirthdayReadScope(completion: completion)
/// - parameter viewController: A `UIViewController` to use while presenting the flow.
/// - returns: A `GIDGoogleUser` with the authorized scope.
/// - throws: Any error that may arise while adding the read birthday scope.
func addBirthdayReadScope(viewController: UIViewController) async throws -> GIDGoogleUser {
return try await authenticator.addBirthdayReadScope(viewController: viewController)
}
#endif

#if os(macOS)
/// adds the requested birthday read scope.
/// - parameter window: An `NSWindow` to use while presenting the flow.
/// - returns: A `GIDGoogleUser` with the authorized scope.
/// - throws: Any error that may arise while adding the read birthday scope.
func addBirthdayReadScope(window: NSWindow) async throws -> GIDGoogleUser {
return try await authenticator.addBirthdayReadScope(window: window)
}
#endif
}

extension AuthenticationViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import Combine
import Foundation

/// An observable class representing the current user's `Birthday` and the number of days until that date.
/// An observable class representing the current user's `Birthday` and the number of days until that
/// date.
final class BirthdayViewModel: ObservableObject {
/// The `Birthday` of the current user.
/// - note: Changes to this property will be published to observers.
Expand All @@ -40,17 +41,12 @@ final class BirthdayViewModel: ObservableObject {

/// Fetches the birthday of the current user.
func fetchBirthday() {
birthdayLoader.birthdayPublisher { publisher in
self.cancellable = publisher.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
self.birthday = Birthday.noBirthday
print("Error retrieving birthday: \(error)")
}
} receiveValue: { birthday in
self.birthday = birthday
Task { @MainActor in
do {
self.birthday = try await birthdayLoader.loadBirthday()
} catch {
print("Error retrieving birthday: \(error)")
self.birthday = .noBirthday
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import GoogleSignInSwift

struct SignInView: View {
@EnvironmentObject var authViewModel: AuthenticationViewModel
@ObservedObject var vm = GoogleSignInButtonViewModel()
@StateObject var vm = GoogleSignInButtonViewModel()

var body: some View {
VStack {
Expand Down
Loading

0 comments on commit db1677e

Please sign in to comment.