Skip to content

Commit db1677e

Browse files
committed
Use async await API in DaysUntilBirthday Sample App
1 parent e46e0cb commit db1677e

File tree

7 files changed

+231
-197
lines changed

7 files changed

+231
-197
lines changed

Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift

Lines changed: 34 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import Combine
1818
import GoogleSignIn
1919

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

54-
private func sessionWithFreshToken(completion: @escaping (Result<URLSession, Error>) -> Void) {
55-
let authentication = GIDSignIn.sharedInstance.currentUser?.authentication
56-
authentication?.do { auth, error in
57-
guard let token = auth?.accessToken else {
58-
completion(.failure(.couldNotCreateURLSession(error)))
59-
return
60-
}
61-
let configuration = URLSessionConfiguration.default
62-
configuration.httpAdditionalHeaders = [
63-
"Authorization": "Bearer \(token)"
64-
]
65-
let session = URLSession(configuration: configuration)
66-
completion(.success(session))
54+
private func sessionWithFreshToken() async throws -> URLSession {
55+
guard let authentication = GIDSignIn.sharedInstance.currentUser?.authentication else {
56+
throw Error.noCurrentUserForSessionWithFreshToken
6757
}
58+
59+
let freshAuth = try await authentication.doWithFreshTokens()
60+
let configuration = URLSessionConfiguration.default
61+
configuration.httpAdditionalHeaders = [
62+
"Authorization": "Bearer \(freshAuth.accessToken)"
63+
]
64+
let session = URLSession(configuration: configuration)
65+
return session
6866
}
6967

70-
/// Creates a `Publisher` to fetch a user's `Birthday`.
71-
/// - parameter completion: A closure passing back the `AnyPublisher<Birthday, Error>`
72-
/// upon success.
73-
/// - note: The `AnyPublisher` passed back through the `completion` closure is created with a
74-
/// fresh token. See `sessionWithFreshToken(completion:)` for more details.
75-
func birthdayPublisher(completion: @escaping (AnyPublisher<Birthday, Error>) -> Void) {
76-
sessionWithFreshToken { [weak self] result in
77-
switch result {
78-
case .success(let authSession):
79-
guard let request = self?.request else {
80-
return completion(Fail(error: .couldNotCreateURLRequest).eraseToAnyPublisher())
68+
/// Fetches a `Birthday`.
69+
/// - returns An instance of `Birthday`.
70+
/// - throws: An instance of `BirthdayLoader.Error` arising while fetching a birthday.
71+
func loadBirthday() async throws -> Birthday {
72+
let session = try await sessionWithFreshToken()
73+
guard let request = request else {
74+
throw Error.couldNotCreateURLRequest
75+
}
76+
let birthdayData = try await withCheckedThrowingContinuation {
77+
(continuation: CheckedContinuation<Data, Swift.Error>) -> Void in
78+
let task = session.dataTask(with: request) { data, response, error in
79+
guard let data = data else {
80+
return continuation.resume(throwing: error ?? Error.noBirthdayData)
8181
}
82-
let bdayPublisher = authSession.dataTaskPublisher(for: request)
83-
.tryMap { data, error -> Birthday in
84-
let decoder = JSONDecoder()
85-
let birthdayResponse = try decoder.decode(BirthdayResponse.self, from: data)
86-
return birthdayResponse.firstBirthday
87-
}
88-
.mapError { error -> Error in
89-
guard let loaderError = error as? Error else {
90-
return Error.couldNotFetchBirthday(underlying: error)
91-
}
92-
return loaderError
93-
}
94-
.receive(on: DispatchQueue.main)
95-
.eraseToAnyPublisher()
96-
completion(bdayPublisher)
97-
case .failure(let error):
98-
completion(Fail(error: error).eraseToAnyPublisher())
82+
continuation.resume(returning: data)
9983
}
84+
task.resume()
10085
}
86+
let decoder = JSONDecoder()
87+
let birthdayResponse = try decoder.decode(BirthdayResponse.self, from: birthdayData)
88+
return birthdayResponse.firstBirthday
10189
}
10290
}
10391

10492
extension BirthdayLoader {
105-
/// An error representing what went wrong in fetching a user's number of day until their birthday.
93+
/// An error for what went wrong in fetching a user's number of days until their birthday.
10694
enum Error: Swift.Error {
107-
case couldNotCreateURLSession(Swift.Error?)
95+
case noCurrentUserForSessionWithFreshToken
10896
case couldNotCreateURLRequest
109-
case userHasNoBirthday
110-
case couldNotFetchBirthday(underlying: Swift.Error)
97+
case noBirthdayData
11198
}
11299
}

Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift

Lines changed: 60 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@
1616

1717
import Foundation
1818
import GoogleSignIn
19+
#if os(iOS)
20+
import UIKit
21+
#elseif os(macOS)
22+
import AppKit
23+
#endif
1924

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

41-
/// Signs in the user based upon the selected account.'
42-
/// - note: Successful calls to this will set the `authViewModel`'s `state` property.
43-
func signIn() {
44-
#if os(iOS)
45-
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
46-
print("There is no root view controller!")
47-
return
48-
}
49-
50-
GIDSignIn.sharedInstance.signIn(with: configuration,
51-
presenting: rootViewController) { user, error in
52-
guard let user = user else {
53-
print("Error! \(String(describing: error))")
54-
return
55-
}
56-
self.authViewModel.state = .signedIn(user)
57-
}
5846

59-
#elseif os(macOS)
60-
guard let presentingWindow = NSApplication.shared.windows.first else {
61-
print("There is no presenting window!")
62-
return
63-
}
47+
#if os(iOS)
48+
/// Signs in the user based upon the selected account.
49+
/// - parameter rootViewController: The `UIViewController` to use during the sign in flow.
50+
/// - returns: The signed in `GIDGoogleUser`.
51+
/// - throws: Any error that may arise during the sign in process.
52+
func signIn(with rootViewController: UIViewController) async throws -> GIDGoogleUser {
53+
return try await GIDSignIn.sharedInstance.signIn(
54+
with: configuration,
55+
presenting: rootViewController
56+
)
57+
}
58+
#endif
6459

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

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

8279
/// Disconnects the previously granted scope and signs the user out.
83-
func disconnect() {
84-
GIDSignIn.sharedInstance.disconnect { error in
85-
if let error = error {
86-
print("Encountered error disconnecting scope: \(error).")
87-
}
88-
self.signOut()
89-
}
80+
func disconnect() async throws {
81+
try await GIDSignIn.sharedInstance.disconnect()
9082
}
9183

92-
// Confines birthday calucation to iOS for now.
84+
#if os(iOS)
9385
/// Adds the birthday read scope for the current user.
94-
/// - parameter completion: An escaping closure that is called upon successful completion of the
95-
/// `addScopes(_:presenting:)` request.
96-
/// - note: Successful requests will update the `authViewModel.state` with a new current user that
97-
/// has the granted scope.
98-
func addBirthdayReadScope(completion: @escaping () -> Void) {
99-
#if os(iOS)
100-
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
101-
fatalError("No root view controller!")
102-
}
103-
104-
GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope],
105-
presenting: rootViewController) { user, error in
106-
if let error = error {
107-
print("Found error while adding birthday read scope: \(error).")
108-
return
109-
}
110-
111-
guard let currentUser = user else { return }
112-
self.authViewModel.state = .signedIn(currentUser)
113-
completion()
114-
}
115-
116-
#elseif os(macOS)
117-
guard let presentingWindow = NSApplication.shared.windows.first else {
118-
fatalError("No presenting window!")
119-
}
120-
121-
GIDSignIn.sharedInstance.addScopes([BirthdayLoader.birthdayReadScope],
122-
presenting: presentingWindow) { user, error in
123-
if let error = error {
124-
print("Found error while adding birthday read scope: \(error).")
125-
return
126-
}
127-
128-
guard let currentUser = user else { return }
129-
self.authViewModel.state = .signedIn(currentUser)
130-
completion()
131-
}
86+
/// - parameter viewController: The `UIViewController` to use while authorizing the scope.
87+
/// - returns: The `GIDGoogleUser` with the authorized scope.
88+
/// - throws: Any error that may arise while authorizing the scope.
89+
func addBirthdayReadScope(viewController: UIViewController) async throws -> GIDGoogleUser {
90+
return try await GIDSignIn.sharedInstance.addScopes(
91+
[BirthdayLoader.birthdayReadScope],
92+
presenting: viewController
93+
)
94+
}
95+
#endif
13296

133-
#endif
97+
#if os(macOS)
98+
/// Adds the birthday read scope for the current user.
99+
/// - parameter window: The `NSWindow` to use while authorizing the scope.
100+
/// - returns: The `GIDGoogleUser` with the authorized scope.
101+
/// - throws: Any error that may arise while authorizing the scope.
102+
func addBirthdayReadScope(window: NSWindow) async throws -> GIDGoogleUser {
103+
return try await GIDSignIn.sharedInstance.addScopes(
104+
[BirthdayLoader.birthdayReadScope],
105+
presenting: window
106+
)
134107
}
108+
#endif
109+
}
135110

111+
extension GoogleSignInAuthenticator {
112+
enum Error: Swift.Error {
113+
case failedToSignIn
114+
case failedToAddBirthdayReadScope(Swift.Error)
115+
case userUnexpectedlyNilWhileAddingBirthdayReadScope
116+
}
136117
}

Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,35 @@ final class AuthenticationViewModel: ObservableObject {
4747

4848
/// Signs the user in.
4949
func signIn() {
50-
authenticator.signIn()
50+
#if os(iOS)
51+
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
52+
print("There is no root view controller!")
53+
return
54+
}
55+
56+
Task { @MainActor in
57+
do {
58+
let user = try await authenticator.signIn(with: rootViewController)
59+
self.state = .signedIn(user)
60+
} catch {
61+
print("Error signing in: \(error)")
62+
}
63+
}
64+
#elseif os(macOS)
65+
guard let presentingWindow = NSApplication.shared.windows.first else {
66+
print("There is no presenting window!")
67+
return
68+
}
69+
70+
Task { @MainActor in
71+
do {
72+
let user = try await authenticator.signIn(with: presentingWindow)
73+
self.state = .signedIn(user)
74+
} catch {
75+
print("Error signing in: \(error)")
76+
}
77+
}
78+
#endif
5179
}
5280

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

5886
/// Disconnects the previously granted scope and logs the user out.
5987
func disconnect() {
60-
authenticator.disconnect()
88+
Task { @MainActor in
89+
do {
90+
try await authenticator.disconnect()
91+
authenticator.signOut()
92+
} catch {
93+
print("Error disconnecting: \(error)")
94+
}
95+
}
6196
}
6297

6398
var hasBirthdayReadScope: Bool {
6499
return authorizedScopes.contains(BirthdayLoader.birthdayReadScope)
65100
}
66101

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

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

75123
extension AuthenticationViewModel {

Samples/Swift/DaysUntilBirthday/Shared/ViewModels/BirthdayViewModel.swift

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import Combine
1818
import Foundation
1919

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

4142
/// Fetches the birthday of the current user.
4243
func fetchBirthday() {
43-
birthdayLoader.birthdayPublisher { publisher in
44-
self.cancellable = publisher.sink { completion in
45-
switch completion {
46-
case .finished:
47-
break
48-
case .failure(let error):
49-
self.birthday = Birthday.noBirthday
50-
print("Error retrieving birthday: \(error)")
51-
}
52-
} receiveValue: { birthday in
53-
self.birthday = birthday
44+
Task { @MainActor in
45+
do {
46+
self.birthday = try await birthdayLoader.loadBirthday()
47+
} catch {
48+
print("Error retrieving birthday: \(error)")
49+
self.birthday = .noBirthday
5450
}
5551
}
5652
}

Samples/Swift/DaysUntilBirthday/Shared/Views/SignInView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import GoogleSignInSwift
1919

2020
struct SignInView: View {
2121
@EnvironmentObject var authViewModel: AuthenticationViewModel
22-
@ObservedObject var vm = GoogleSignInButtonViewModel()
22+
@StateObject var vm = GoogleSignInButtonViewModel()
2323

2424
var body: some View {
2525
VStack {

0 commit comments

Comments
 (0)