SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)

版本记录

版本号 时间
V1.0 2019.10.18 星期五

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)

开始

首先看下工程组织结构

SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)_第1张图片

接着就是源码了

1. WebApi.swift
import Foundation

struct WebApi {
  static func Register(user: UserData, identityToken: Data?, authorizationCode: Data?) throws -> Bool {
    return true
  }
}
2. ContentView.swift
import UIKit
import SwiftUI
import AuthenticationServices

struct ContentView: View {
  @Environment(\.window) var window: UIWindow?
  @State var appleSignInDelegates: SignInWithAppleDelegates! = nil

  var body: some View {
    ZStack {
      Color.green.edgesIgnoringSafeArea(.all)

      VStack {
        Image("razeware")

        UserAndPassword()
          .padding()

        SignInWithApple()
          .frame(width: 280, height: 60)
          .onTapGesture(perform: showAppleLogin)
      }
    }
    .onAppear {
      self.performExistingAccountSetupFlows()
    }
  }

  private func showAppleLogin() {
    let request = ASAuthorizationAppleIDProvider().createRequest()
    request.requestedScopes = [.fullName, .email]

    performSignIn(using: [request])
  }

  /// Prompts the user if an existing iCloud Keychain credential or Apple ID credential is found.
  private func performExistingAccountSetupFlows() {
    #if !targetEnvironment(simulator)
    // Note that this won't do anything in the simulator.  You need to
    // be on a real device or you'll just get a failure from the call.
    let requests = [
      ASAuthorizationAppleIDProvider().createRequest(),
      ASAuthorizationPasswordProvider().createRequest()
    ]

    performSignIn(using: requests)
    #endif
  }

  private func performSignIn(using requests: [ASAuthorizationRequest]) {
    appleSignInDelegates = SignInWithAppleDelegates(window: window) { success in
      if success {
        // update UI
      } else {
        // show the user an error
      }
    }

    let controller = ASAuthorizationController(authorizationRequests: requests)
    controller.delegate = appleSignInDelegates
    controller.presentationContextProvider = appleSignInDelegates

    controller.performRequests()
  }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
#endif
3. Keychain.swift
import Foundation

enum KeychainError: Error {
  case secCallFailed(OSStatus)
  case notFound
  case badData
  case archiveFailure(Error)
}

protocol Keychain {
  associatedtype DataType: Codable

  var account: String { get set }
  var service: String { get set }

  func remove() throws
  func retrieve() throws -> DataType
  func store(_ data: DataType) throws
}

extension Keychain {
  func remove() throws {
    let status = SecItemDelete(keychainQuery() as CFDictionary)
    guard status == noErr || status == errSecItemNotFound else {
      throw KeychainError.secCallFailed(status)
    }
  }

  func retrieve() throws -> DataType {
    var query = keychainQuery()
    query[kSecMatchLimit as String] = kSecMatchLimitOne
    query[kSecReturnAttributes as String] = kCFBooleanTrue
    query[kSecReturnData as String] = kCFBooleanTrue

    var result: AnyObject?
    let status = withUnsafeMutablePointer(to: &result) {
      SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
    }

    guard status != errSecItemNotFound else { throw KeychainError.notFound }
    guard status == noErr else { throw KeychainError.secCallFailed(status) }

    do {
      guard
        let dict = result as? [String: AnyObject],
        let data = dict[kSecAttrGeneric as String] as? Data,
        let userData = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? DataType
        else {
          throw KeychainError.badData
      }

      return userData
    } catch {
      throw KeychainError.archiveFailure(error)
    }
  }

  func store(_ data: DataType) throws {
    var query = keychainQuery()

    let archived: AnyObject
    do {
      archived = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true) as AnyObject
    } catch {
      throw KeychainError.archiveFailure(error)
    }

    let status: OSStatus
    do {
      // If doesn't already exist, this will throw a KeychainError.notFound,
      // causing the catch block to add it.
       _ = try retrieve()

      let updates = [
        String(kSecAttrGeneric): archived
      ]

      status = SecItemUpdate(query as CFDictionary, updates as CFDictionary)
    } catch KeychainError.notFound {
      query[kSecAttrGeneric as String] = archived
      status = SecItemAdd(query as CFDictionary, nil)
    } 

    guard status == noErr else {
      throw KeychainError.secCallFailed(status)
    }
  }

  private func keychainQuery() -> [String: AnyObject] {
    var query: [String: AnyObject] = [:]
    query[kSecClass as String] = kSecClassGenericPassword
    query[kSecAttrService as String] = service as AnyObject
    query[kSecAttrAccount as String] = account as AnyObject

    return query
  }
}
4. SharedWebCredential.swift
import Foundation

enum SharedWebCredentialError: Error {
  case SecRequestFailure(CFError)
  case MissingCredentials
  case ConversionFailure
}

struct SharedWebCredential {
  private let domain: CFString
  private let safeForTutorialCode: Bool

  init(domain: String) {
    self.domain = domain as CFString
    safeForTutorialCode = !domain.isEmpty
  }

  func retrieve(account: String? = nil, completion: @escaping (Result<(account: String?, password: String?), SharedWebCredentialError>) -> Void) {
    guard safeForTutorialCode else {
      print("Please set your domain for SharedWebCredential constructor in UserAndPassword.swift!")
      return
    }

    var acct: CFString? = nil

    if let account = account {
      acct = account as CFString
    }

    SecRequestSharedWebCredential(domain, acct) { credentials, error in
      if let error = error {
        DispatchQueue.main.async {
          completion(.failure(.SecRequestFailure(error)))
        }

        return
      }

      guard
        let credentials = credentials,
        CFArrayGetCount(credentials) > 0
        else {
          DispatchQueue.main.async {
            completion(.failure(.MissingCredentials))
          }
          
          return
      }

      let unsafeCredential = CFArrayGetValueAtIndex(credentials, 0)
      let credential: CFDictionary = unsafeBitCast(unsafeCredential, to: CFDictionary.self)
      guard let dict = credential as? Dictionary else {
        DispatchQueue.main.async {
          completion(.failure(.ConversionFailure))
        }

        return
      }

      let username = dict[kSecAttrAccount as String]
      let password = dict[kSecSharedPassword as String]

      DispatchQueue.main.async {
        completion(.success((username, password)))
      }
    }
  }

  private func update(account: String, password: String?, completion: @escaping (Result) -> Void) {
    guard safeForTutorialCode else {
      print("Please set your domain for SharedWebCredential constructor in UserAndPassword.swift!")
      return
    }

    var pwd: CFString? = nil
    if let password = password {
      pwd = password as CFString
    }

    SecAddSharedWebCredential(domain, account as CFString, pwd) { error in
      DispatchQueue.main.async {
        if let error = error {
          completion(.failure(.SecRequestFailure(error)))
        } else {
          completion(.success(true))
        }
      }
    }
  }

  func store(account: String, password: String, completion: @escaping (Result) -> Void) {
    update(account: account, password: password, completion: completion)
  }

  func delete(account: String, completion: @escaping (Result) -> Void) {
    update(account: account, password: nil, completion: completion)
  }
}
5. UserData.swift
import Foundation

/// Represents the details about the user which were provided during initial registration.
struct UserData: Codable {
  /// The email address to use for user communications.  Remember it might be a relay!
  let email: String

  /// The components which make up the user's name.  See `displayName(style:)`
  let name: PersonNameComponents

  /// The team scoped identifier Apple provided to represent this user.
  let identifier: String

  /// Returns the localized name for the person
  /// - Parameter style: The `PersonNameComponentsFormatter.Style` to use for the display.
  func displayName(style: PersonNameComponentsFormatter.Style = .default) -> String {
    PersonNameComponentsFormatter.localizedString(from: name, style: style)
  }
}
6. UserDataKeychain.swift
import Foundation

struct UserDataKeychain: Keychain {
  // Make sure the account name doesn't match the bundle identifier!
  var account = "com.raywenderlich.SignInWithApple.Details"
  var service = "userIdentifier"

  typealias DataType = UserData
}
7. EnvironmentWindowKey.swift
import UIKit
import SwiftUI

struct WindowKey: EnvironmentKey {
  struct Value {
    weak var value: UIWindow?
  }
  
  static let defaultValue: Value = .init(value: nil)
}

extension EnvironmentValues {
  var window: UIWindow? {
    get { return self[WindowKey.self].value }
    set { self[WindowKey.self] = .init(value: newValue) }
  }
}
8. SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder {
  var window: UIWindow?
}

extension SceneDelegate: UIWindowSceneDelegate {
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = scene as? UIWindowScene else { return }
    
    let window = UIWindow(windowScene: windowScene)

    let rootView = ContentView().environment(\.window, window)
    
    window.rootViewController = UIHostingController(rootView: rootView)
    self.window = window
    window.makeKeyAndVisible()
  }
}
9. SignInWithApple.swift
import SwiftUI
import AuthenticationServices

final class SignInWithApple: UIViewRepresentable {
  func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
    return ASAuthorizationAppleIDButton()
  }
  
  func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
  }
}
10. SignInWithAppleDelegates.swift
import UIKit
import AuthenticationServices
import Contacts

class SignInWithAppleDelegates: NSObject {
  private let signInSucceeded: (Bool) -> Void
  private weak var window: UIWindow!
  
  init(window: UIWindow?, onSignedIn: @escaping (Bool) -> Void) {
    self.window = window
    self.signInSucceeded = onSignedIn
  }
}

extension SignInWithAppleDelegates: ASAuthorizationControllerDelegate {
  private func registerNewAccount(credential: ASAuthorizationAppleIDCredential) {
    // 1
    let userData = UserData(email: credential.email!,
                            name: credential.fullName!,
                            identifier: credential.user)

    // 2
    let keychain = UserDataKeychain()
    do {
      try keychain.store(userData)
    } catch {
      self.signInSucceeded(false)
    }

    // 3
    do {
      let success = try WebApi.Register(user: userData,
                                        identityToken: credential.identityToken,
                                        authorizationCode: credential.authorizationCode)
      self.signInSucceeded(success)
    } catch {
      self.signInSucceeded(false)
    }
  }

  private func signInWithExistingAccount(credential: ASAuthorizationAppleIDCredential) {
    // You *should* have a fully registered account here.  If you get back an error from your server
    // that the account doesn't exist, you can look in the keychain for the credentials and rerun setup

    // if (WebAPI.Login(credential.user, credential.identityToken, credential.authorizationCode)) {
    //   ...
    // }
    self.signInSucceeded(true)
  }

  private func signInWithUserAndPassword(credential: ASPasswordCredential) {
    // You *should* have a fully registered account here.  If you get back an error from your server
    // that the account doesn't exist, you can look in the keychain for the credentials and rerun setup

    // if (WebAPI.Login(credential.user, credential.password)) {
    //   ...
    // }
    self.signInSucceeded(true)
  }
  
  func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    switch authorization.credential {
    case let appleIdCredential as ASAuthorizationAppleIDCredential:
      if let _ = appleIdCredential.email, let _ = appleIdCredential.fullName {
        registerNewAccount(credential: appleIdCredential)
      } else {
        signInWithExistingAccount(credential: appleIdCredential)
      }

      break
      
    case let passwordCredential as ASPasswordCredential:
      signInWithUserAndPassword(credential: passwordCredential)

      break
      
    default:
      break
    }
  }
  
  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    // Handle error.
  }
}

extension SignInWithAppleDelegates: ASAuthorizationControllerPresentationContextProviding {
  func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
    return self.window
  }
}
11. UserAndPassword.swift
import SwiftUI

struct UserAndPassword: View {
  @State var username: String = ""
  @State var password: String = ""
  @State var showingAlert = false
  @State var alertText: String = ""

  var body: some View {
    VStack {
      TextField("Username", text: $username)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .textContentType(.username)
        .autocapitalization(.none)
        .disableAutocorrection(true)

      SecureField("Password", text: $password)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .textContentType(.password)

      Button(action: signInTapped) {
        Text("Log In")
          .foregroundColor(Color.white)
      }
      .alert(isPresented: $showingAlert) {
        Alert(title: Text(alertText))
      }
    }
    .padding()
  }

  private func signInTapped() {
    let ws = CharacterSet.whitespacesAndNewlines

    let account = username.trimmingCharacters(in: ws)
    let pwd = password.trimmingCharacters(in: ws)

    guard !(account.isEmpty || pwd.isEmpty) else {
      alertText = "Please enter a username and password."
      showingAlert = true
      return
    }
    
    // Putting the user/pwd into the shared web credentials ensures that
    // it's available for your browser based logins if you haven't implemented
    // the web version of Sign in with Apple but also then makes it available
    // for future logins via Sign in with Apple on your iOS devices.
    SharedWebCredential(domain: "")
      .store(account: account, password: password) { result in
        guard case .failure = result else { return }

        self.alertText = "Failed to store password."
        self.showingAlert = true
    }
  }
}

#if DEBUG
struct UserAndPassword_Previews: PreviewProvider {
  static var previews: some View {
    UserAndPassword()
  }
}
#endif

后记

本篇主要讲述了使用SwiftUI进行苹果登录,感兴趣的给个赞或者关注~~~

SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)_第2张图片

你可能感兴趣的:(SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二))