瀏覽代碼

Update DaysUntilBirthday sample app to use async await API

Matthew Mathias 3 年之前
父節點
當前提交
b4544e35c5

+ 2 - 0
Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj

@@ -656,6 +656,7 @@
 				COMBINE_HIDPI_IMAGES = YES;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "\"macOS/Preview Content\"";
+				DEVELOPMENT_TEAM = "";
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = macOS/Info.plist;
@@ -685,6 +686,7 @@
 				COMBINE_HIDPI_IMAGES = YES;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "\"macOS/Preview Content\"";
+				DEVELOPMENT_TEAM = "";
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = macOS/Info.plist;

+ 31 - 42
Samples/Swift/DaysUntilBirthday/Shared/Services/BirthdayLoader.swift

@@ -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"
@@ -51,20 +51,18 @@ 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`.
@@ -72,41 +70,32 @@ final class BirthdayLoader: ObservableObject {
   /// 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())
+  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
   }
 }

+ 59 - 78
Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift

@@ -16,6 +16,11 @@
 
 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 {
@@ -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() {
@@ -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
+  }
 }

+ 53 - 5
Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift

@@ -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.
@@ -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 {

+ 8 - 12
Samples/Swift/DaysUntilBirthday/Shared/ViewModels/BirthdayViewModel.swift

@@ -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.
@@ -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
       }
     }
   }

+ 34 - 22
Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift

@@ -37,25 +37,45 @@ struct UserProfileView: View {
               Text(userProfile.email)
             }
           }
-          NavigationLink(NSLocalizedString("View Days Until Birthday", comment: "View birthday days"),
-                         destination: BirthdayView(birthdayViewModel: birthdayViewModel).onAppear {
-            guard self.birthdayViewModel.birthday != nil else {
-              if !self.authViewModel.hasBirthdayReadScope {
-                self.authViewModel.addBirthdayReadScope {
-                  self.birthdayViewModel.fetchBirthday()
+          NavigationLink(
+            NSLocalizedString("View Days Until Birthday", comment: "View birthday days"),
+            destination: BirthdayView(birthdayViewModel: birthdayViewModel)
+              .onAppear {
+                guard self.birthdayViewModel.birthday != nil else {
+                  if !self.authViewModel.hasBirthdayReadScope {
+                    guard let viewController = UIApplication.shared.windows.first?.rootViewController else {
+                      print("There was no root view controller")
+                      return
+                    }
+                    Task { @MainActor in
+                      do {
+                        let user = try await authViewModel.addBirthdayReadScope(
+                          viewController: viewController
+                        )
+                        self.authViewModel.state = .signedIn(user)
+                        self.birthdayViewModel.fetchBirthday()
+                      } catch {
+                        print("Failed to fetch birthday: \(error)")
+                      }
+                    }
+                  } else {
+                    self.birthdayViewModel.fetchBirthday()
+                  }
+                  return
                 }
-              } else {
-                self.birthdayViewModel.fetchBirthday()
-              }
-              return
-            }
-          })
+              })
           Spacer()
         }
         .toolbar {
           ToolbarItemGroup(placement: .navigationBarTrailing) {
-            Button(NSLocalizedString("Disconnect", comment: "Disconnect button"), action: disconnect)
-            Button(NSLocalizedString("Sign Out", comment: "Sign out button"), action: signOut)
+            Button(
+              NSLocalizedString("Disconnect", comment: "Disconnect button"),
+              action: authViewModel.disconnect
+            )
+            Button(
+              NSLocalizedString("Sign Out", comment: "Sign out button"),
+              action: authViewModel.signOut
+            )
           }
         }
       } else {
@@ -63,12 +83,4 @@ struct UserProfileView: View {
       }
     }
   }
-
-  func disconnect() {
-    authViewModel.disconnect()
-  }
-
-  func signOut() {
-    authViewModel.signOut()
-  }
 }

+ 2 - 0
Samples/Swift/DaysUntilBirthday/macOS/DaysUntilBirthdayOnMac.entitlements

@@ -8,5 +8,7 @@
 	<true/>
 	<key>com.apple.security.network.client</key>
 	<true/>
+	<key>keychain-access-groups</key>
+	<array/>
 </dict>
 </plist>

+ 41 - 31
Samples/Swift/DaysUntilBirthday/macOS/UserProfileView.swift

@@ -21,32 +21,50 @@ struct UserProfileView: View {
               Text(userProfile.email)
             }
           }
-          Button(NSLocalizedString("Sign Out", comment: "Sign out button"), action: signOut)
-            .background(Color.blue)
-            .foregroundColor(Color.white)
-            .cornerRadius(5)
+          Button(
+            NSLocalizedString("Sign Out", comment: "Sign out button"),
+            action: authViewModel.signOut
+          )
+          .background(Color.blue)
+          .foregroundColor(Color.white)
+          .cornerRadius(5)
 
-          Button(NSLocalizedString("Disconnect", comment: "Disconnect button"), action: disconnect)
-            .background(Color.blue)
-            .foregroundColor(Color.white)
-            .cornerRadius(5)
+          Button(
+            NSLocalizedString("Disconnect", comment: "Disconnect button"),
+            action: authViewModel.disconnect
+          )
+          .background(Color.blue)
+          .foregroundColor(Color.white)
+          .cornerRadius(5)
           Spacer()
-          NavigationLink(NSLocalizedString("View Days Until Birthday", comment: "View birthday days"),
-                         destination: BirthdayView(birthdayViewModel: birthdayViewModel).onAppear {
-            guard self.birthdayViewModel.birthday != nil else {
-              if !self.authViewModel.hasBirthdayReadScope {
-                self.authViewModel.addBirthdayReadScope {
-                  self.birthdayViewModel.fetchBirthday()
+          NavigationLink(
+            NSLocalizedString("View Days Until Birthday", comment: "View birthday days"),
+            destination: BirthdayView(birthdayViewModel: birthdayViewModel)
+              .onAppear {
+                guard self.birthdayViewModel.birthday != nil else {
+                  if !self.authViewModel.hasBirthdayReadScope {
+                    guard let window = NSApplication.shared.windows.first else {
+                      print("There was no presenting window")
+                      return
+                    }
+                    Task { @MainActor in
+                      do {
+                        let user = try await authViewModel.addBirthdayReadScope(window: window)
+                        self.authViewModel.state = .signedIn(user)
+                        self.birthdayViewModel.fetchBirthday()
+                      } catch {
+                        print("Failed to fetch birthday: \(error)")
+                      }
+                    }
+                  } else {
+                    self.birthdayViewModel.fetchBirthday()
+                  }
+                  return
                 }
-              } else {
-                self.birthdayViewModel.fetchBirthday()
-              }
-              return
-            }
-          })
-            .background(Color.blue)
-            .foregroundColor(Color.white)
-            .cornerRadius(5)
+              })
+          .background(Color.blue)
+          .foregroundColor(Color.white)
+          .cornerRadius(5)
           Spacer()
         }
       } else {
@@ -54,12 +72,4 @@ struct UserProfileView: View {
       }
     }
   }
-
-  func disconnect() {
-    authViewModel.disconnect()
-  }
-
-  func signOut() {
-    authViewModel.signOut()
-  }
 }