###### tags: `第14屆IT邦鐵人賽文章` # 【在 iOS 開發路上的大小事2-Day18】Password AutoFill App Extension ## 前情提要 在上一篇有簡單介紹了 Application Extension,在這篇會用 AutoFill Credential Provider 來做示範 AutoFill Credential Provider 是在 iOS 12 出現的 Application Extension ## 實作前的提醒 1. 專案的開發者帳號需為 **付費開發者帳號** 2. AutoFill Credential Provider 只支援下面這幾個平台 * iOS 12.0+ * iPadOS 12.0+ * macOS 11.0+ * Mac Catalyst 14.0+ ## 開始實作 ### 新增 AutoFill Credential Provider Target 首先在專案的 TARGET 內點擊左下角的「+」 ![](https://i.imgur.com/1cq2vYG.png) 在 Application Extension 中選擇「AutoFill Credential Provider」 ![](https://i.imgur.com/8SEspeS.png) 輸入 Application Extension 的 Target 資訊,好了之後按 Finish ![](https://i.imgur.com/c57pVj1.png) 接著,Xcode 會問你說,要不要啟動這個 Extension 的 Scheme 這裡就按 Activate,這樣方便後面進行測試 ![](https://i.imgur.com/A0AidKU.png) 現在,就可以在 TARGET 內看到剛才建立的 AutoFill Credential Provider 這個 Extension 了 另外也會建立一個專屬 Application Extension 的資料夾,來存放 Extension 的相關檔案 (紅框處) ![](https://i.imgur.com/Bun07vD.png) ### AutoFill Credential Provider Application Extension 檔案說明 * [CredentialProviderViewController.swift](#CredentialProviderViewController) * 說明:Host App 觸發 App Extension 後跳出的畫面 (Code) * 繼承 ASCredentialProviderViewController 這個 class ![](https://i.imgur.com/4YoqIXU.png) * MainInterface.storyboard * 說明:Host App 觸發 App Extension 後跳出的畫面 (UI) * 如果要換成用 Xib 設計的話,需要到 Extension 的 Info.plist 修改 (後面會提到) ![](https://i.imgur.com/FBFO01E.png) * [Info.plist](#Infoplist) (不是 Containing App 的喔,是 App Extension 的!) * 說明:用來配置 Application Extension 的 ![](https://i.imgur.com/4Fb0TMZ.png) * PasswordAutoFillExtension.entitlements * 說明:Application Extension 的功能設定檔 ![](https://i.imgur.com/u0uPkBj.png) ### CredentialProviderViewController 預設建好的 CredentialProviderViewController 是繼承於 ASCredentialProviderViewController 而 ASCredentialProviderViewController 又是繼承於 UIViewController ```swift /// iOS、iPadOS、Mac Catalyst class ASCredentialProviderViewController : UIViewController /// macOS class ASCredentialProviderViewController : NSViewController ``` 接著來看一下 ASCredentialProviderViewController 提供了哪些 Function 來讓我們使用 ```swift @available(iOS 12.0, *) open class ASCredentialProviderViewController : UIViewController { /// AutoFill Credential Provider Extension 用來提供資訊給系統的上下文 open var extensionContext: ASCredentialProviderExtensionContext { get } /// 用來準備 Credential List 介面給使用者選擇 /// Host App 呼叫 Extension 時所在的網頁網址可以透過這個 Function 來取得 open func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) /// 用來在不提供使用者互動的情況下,在鍵盤上的 QuickType bar 提供 Credential,給使用者選取 open func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) /// 當在不提供使用者互動的情況下,無法提供 Credential 的話,系統會呼叫這個 Function 來顯示使用者互動介面 /// 像是生物驗證、資料庫登入等,並回傳對應的 Credential Identity open func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) /// 使用者在設定內啟用 Containing App 的 App Extension 後,顯示的畫面 /// 需在 Info.plist 內新增參數來設定 open func prepareInterfaceForExtensionConfiguration() } ``` 看完上面的說明後,就來看一下預設建好的 CredentialProviderViewController 裡面長什麼樣子 ```swift import AuthenticationServices class CredentialProviderViewController: ASCredentialProviderViewController { /* Prepare your UI to list available credentials for the user to choose from. The items in 'serviceIdentifiers' describe the service the user is logging in to, so your extension can prioritize the most relevant credentials in the list. */ override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { } /* Implement this method if your extension supports showing credentials in the QuickType bar. When the user selects a credential from your app, this method will be called with the ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore. Provide the password by completing the extension request with the associated ASPasswordCredential. If using the credential would require showing custom UI for authenticating the user, cancel the request with error code ASExtensionError.userInteractionRequired. override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { let databaseIsUnlocked = true if (databaseIsUnlocked) { let passwordCredential = ASPasswordCredential(user: "zaqxsw0218", password: "zaqxsw0218") self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) } else { self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userInteractionRequired.rawValue)) } } */ /* Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with ASExtensionError.userInteractionRequired. In this case, the system may present your extension's UI and call this method. Show appropriate UI for authenticating the user then provide the password by completing the extension request with the associated ASPasswordCredential. override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { } */ @IBAction func cancel(_ sender: AnyObject?) { self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) } @IBAction func passwordSelected(_ sender: AnyObject?) { let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) } } ``` 如果直接執行的話,也是可以的喔,執行結果會長得像下面這樣 ![](https://i.imgur.com/XldyrAa.jpg =360x640) ▲ Host App 呼叫 App Extension 後顯示的預設畫面 #### 取得 Host App 呼叫 App Extension 時的網頁網址 上面有說到可以透過下面這個 Function 來取得 ```swift open func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) ``` 那實際是要怎麼取得呢?取得的方法也很簡單 ```swift var serviceIdentifier: String = "" override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { /// serviceIdentifiers 是一個陣列 /// 而網頁的網址 (identifier) 就藏在這個陣列裡面 /// 取第一個是因為更精確的網址會排序在第一個 serviceIdentifier = serviceIdentifiers[0].identifier print(serviceIdentifiers[0].identifier) } ``` #### 將 Credential 結合鍵盤上的 QuickType bar 要將 Credential 與 QuickType bar 結合,就必須要有 ASPasswordCredentialIdentity 那 ASPasswordCredentialIdentity 要怎麼生出來呢,就得先看他需要什麼東西來 init 了 ```swift @available(iOS 12.0, *) open class ASPasswordCredentialIdentity : NSObject, NSCopying, NSSecureCoding { public init(serviceIdentifier: ASCredentialServiceIdentifier, user: String, recordIdentifier: String?) ... } ``` ASPasswordCredentialIdentity 需要傳入下面三個參數來 init ```swift /// 在 prepareCredentialList 所取得的 serviceIdentifier serviceIdentifier: ASCredentialServiceIdentifier /// 使用者名稱,會顯示在 QuickType bar 上 user: String /// 與資料庫關聯的 Identifier recordIdentifier: String? ``` 看完 init 所需要的參數後,就可以來實作了 首先是宣告一個 [ASPasswordCredentialIdentity] 用來存放要加入 ASCredentialIdentityStore 物件中的 Credential ```swift let credentialList: [ASPasswordCredentialIdentity] = [ .init(serviceIdentifier: ASCredentialServiceIdentifier(identifier: self.serviceIdentifier, type: .URL), user: "j_appleseed", recordIdentifier: nil) ] ASCredentialIdentityStore.shared.saveCredentialIdentities(credentialList) { success, error in if success { print("Success") } else { print("Error: \(error?.localizedDescription)") } } ``` 將 Credential 存進 ASCredentialIdentityStore 後 就可以透過下面這個 Function 來在 QuickType bar 上用 AutoFill 了 ```swift func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) ``` 下面是 Xcode 裡面預先寫好的範例,當然實際上不會是這樣寫,但方向是對的 ```swift override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { let databaseIsUnlocked = true if (databaseIsUnlocked) { let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) } else { self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userInteractionRequired.rawValue)) } } ``` #### 在「設定 -> 密碼 -> 自動填寫密碼」勾選時顯示自定義畫面 首先,先建立一個 ASCredentialProviderViewController class 的 ViewController (建立完之後,要記得 ```import AuthenticationServices```,不然 Xcode 可是會生氣的喔!) 接著在畫面上放一個 Button 或是 UIBarButtonItem 來呼叫 ```self.extensionContext.completeExtensionConfigurationRequest()``` 接著再回到 CredentialProviderViewController 將 ```func prepareInterfaceForExtensionConfiguration()``` 進行 override ```swift override func prepareInterfaceForExtensionConfiguration() { guard let vc = storyboard?.instantiateViewController(identifier: <YOUR_Storyboard_ID>) else { return } present(vc, animated: true) } ``` **最後,最重要的一步就是,到 Info.plist 裡面新增參數,不然可是不會理你的喔** ```xml <key>NSExtensionAttributes</key> <dict> <key>ASCredentialProviderExtensionShowsConfigurationUI</key> <true/> </dict> ``` ![](https://i.imgur.com/h5OscbD.png) ▲ Info.plist 以「Property List」模式檢視 ![](https://i.imgur.com/KHHs9RS.png) ▲ Info.plist 以「Source Code」模式檢視 這樣就可以在設定裡面勾選時,顯示自定義的完成 Extension 設定畫面了 但是,這樣會有個問題,自定義畫面是顯示了 但如果下滑的話,就會返回看到 App Extension 的第一個畫面了 這樣感覺不行,要想辦法才行!!! 解決方法很簡單,只要在 ```prepareInterfaceForExtensionConfiguration()``` 裡面加一行就可以了 ```swift override func prepareInterfaceForExtensionConfiguration() { guard let vc = storyboard?.instantiateViewController(identifier: <YOUR_Storyboard_ID>) else { return } /// ↓ 這行是用來關閉下滑返回,但有下面那行,這行不加也可以 ↓ vc.isModalInPresentation = true /// ↓ 只要加這行就可以解決了 ↓ vc.modalPresentationStyle = .fullScreen // 將 present 的方式改為全螢幕 present(vc, animated: true) } ``` ### Info.plist 剛建完 AutoFill Credential Provider App Extension 後,Info.plist 預設為長得像下面這張圖一樣 在 NSExtension 這個 Dictionary 裡面,只有 ```NSExtensionMainStoryboard``` 跟 ```NSExtensionPointIdentifier``` 這兩個 key-value ![](https://i.imgur.com/CKw2ahL.png) 在經過顯示自定義畫面後,Info.plist 會長得像下面這樣 ![](https://i.imgur.com/h5OscbD.png) 那如果我不想要用 Storyboard 設計畫面,想改用 Xib 的話,該怎麼修改呢?解法很簡單!!! 只要將 ```NSExtensionMainStoryboard``` 改成 ```NSExtensionPrincipalClass``` 而 ```NSExtensionMainStoryboard``` 的 value 改成 ```$(PRODUCT_MODULE_NAME).<Xib 檔案的 class>``` ```xml <!-- 將 NSExtensionMainStoryboard 的這兩行 --> <key>NSExtensionMainStoryboard</key> <string>MainInterface</string> <!-- 換成 NSExtensionPrincipalClass 的這兩行 --> <key>NSExtensionPrincipalClass</key> <string>$(PRODUCT_MODULE_NAME).TestViewController2</string> ``` ![](https://i.imgur.com/hpT3Kv4.png) ▲ Info.plist 以「Property List」模式檢視 ![](https://i.imgur.com/ROoYb3C.png) ▲ Info.plist 以「Source Code」模式檢視 最後再到 TARGETS -> AutoFill Credential Provider Extension -> Deployment Info 將 MainInterface 清空 ![](https://i.imgur.com/LXKXO2q.png) ## 總結 在這篇實作了 AutoFill Credential Provider Application Extension 如果有想研究密碼管理器的話,這篇應該可以當作參考 (自己說哈哈哈哈哈) 接下來還會有一些主題出現~就讓我們繼續看下去~ ## 參考資料 > 1. https://developer.apple.com/videos/play/wwdc2018/721/ > 2. https://developer.apple.com/documentation/authenticationservices/ascredentialproviderviewcontroller > 3. https://juejin.cn/post/6885176823714414605 > 4. https://www.jianshu.com/p/51601d6af45a > 5. https://www.796t.com/post/MW95NDI=.html