SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋(píng)果登錄(二)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.10.18 星期五

前言

今天翻閱蘋(píng)果的API文檔妈经,發(fā)現(xiàn)多了一個(gè)框架SwiftUI淮野,這里我們就一起來(lái)看一下這個(gè)框架。感興趣的看下面幾篇文章吹泡。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋(píng)果登錄(一)

開(kāi)始

首先看下工程組織結(jié)構(gòu)

接著就是源碼了

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<String, String> 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<Bool, SharedWebCredentialError>) -> 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<Bool, SharedWebCredentialError>) -> Void) {
    update(account: account, password: password, completion: completion)
  }

  func delete(account: String, completion: @escaping (Result<Bool, SharedWebCredentialError>) -> 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進(jìn)行蘋(píng)果登錄骤星,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市爆哑,隨后出現(xiàn)的幾起案子洞难,更是在濱河造成了極大的恐慌,老刑警劉巖揭朝,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件队贱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡潭袱,警方通過(guò)查閱死者的電腦和手機(jī)柱嫌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)屯换,“玉大人编丘,你說(shuō)我怎么就攤上這事√司叮” “怎么了瘪吏?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蜗巧。 經(jīng)常有香客問(wèn)我掌眠,道長(zhǎng),這世上最難降的妖魔是什么幕屹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任蓝丙,我火速辦了婚禮级遭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘渺尘。我一直安慰自己挫鸽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布鸥跟。 她就那樣靜靜地躺著丢郊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪医咨。 梳的紋絲不亂的頭發(fā)上枫匾,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音拟淮,去河邊找鬼干茉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛很泊,可吹牛的內(nèi)容都是我干的角虫。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼委造,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼戳鹅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起争涌,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤粉楚,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后亮垫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體模软,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年饮潦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了燃异。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡继蜡,死狀恐怖回俐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情稀并,我是刑警寧澤仅颇,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站碘举,受9級(jí)特大地震影響忘瓦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜引颈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一耕皮、第九天 我趴在偏房一處隱蔽的房頂上張望境蜕。 院中可真熱鬧,春花似錦凌停、人聲如沸粱年。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)台诗。三九已至,卻和暖如春舟舒,著一層夾襖步出監(jiān)牢的瞬間拉庶,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工秃励, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吉捶。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓夺鲜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親呐舔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子币励,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容