在iOS 8 SDK中使用Touch ID API(上)
執行TouchID認證
在工程和界面設置完成後,我們第一個要做的事情是在應用程序中集成Touch ID認證機制。像我在介紹本教程時提到的,TouchID的用法是基於Local Authentication框架的,但是該框架不在我們的工程中,我們必須先增加它,然後才能實現TouchID的特性。
在Project Navigator,中,點擊Project行並點擊右邊的Build Phases 標簽,你在裡邊會發現一些折疊的選項。我們需要的是Link Binary With Libraries(0 items)。點擊打開圖標展開它,之後點擊小的插入圖標。
在模態窗口中,輸入Local Authentication,Xcode將會將其查找出來。
接下來,選擇它並點擊Add按鈕,框架就會被增加到工程中。現在我們准備寫代碼。返回到ViewController.swift文件,並在頂部導入新框架
import LocalAuthentication
接下來建立一個新的名為authenticateUser():的函數
func authenticateUser() { }
這裡我們將要編寫集成TouchID認證的代碼。像你看到的那樣,我沒有設定方法的返回值,因為它是一個void one。還有,它根本不接受任何參數。
在使用Touch ID和Local Authentication框架時,所需的第一步通常是從框架中獲得認證上下文環境,如下明確展示的:
func authenticateUser() { // Get the local authentication context. let context : LAContext = LAContext() }
注意:以上命令也能夠像這樣寫:
let context = LAContext()
上下文常量類型是推斷出的,我們可以忽略它,然而我個人在這個例子中喜歡第一種方式更多些,它讓很多事情更清晰。除了那些,我們必須定義兩個變量:一個是NSError類型的,一個是String類型的,這樣便於指出展示Touch ID對話框的原因,讓我們增加這幾行代碼:
func authenticateUser() { // Get the local authentication context. let context = LAContext() // Declare a NSError variable. var error: NSError? // Set the reason string that will appear on the authentication alert. var reasonString = "Authentication is needed to access your notes." }
注意:錯誤變量聲明是可選的,因為如果沒有錯誤它將會返回nil,提醒一下,在Swift中nil不同於Objective-C中的nil,它意味著沒有值。還有就是reasonString字符串在編譯器將會從分配的值推斷它時,我會忽略它的類型。reasonString可以自定義,因此可以隨意設置你喜歡的信息。
如果TouchID認證能夠被提交到指定的設備,接下來的步驟便是請求框架,通過調用一個指定的名為canEvaluatePolicy的函數。它接受兩個參數,我們想要評估的策略和錯誤對象。以下是如何使用該函數:
func authenticateUser() { ... // Check if the device can evaluate the policy. if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) { } }
DeviceOwnerAuthenticationWithBiometrics是LAPolicy類對象的一個屬性。注意通過引用傳遞的那個錯誤變量。如果條件是對的,那麼設備支持Touch ID認證,Touch ID機制已經在設備設定中啟用,當然還會設定一個密碼,至少錄入一個指紋。這意味著應用了一個特性的認證策略,並且也會顯示Touch ID認證對話框:
func authenticateUser() { ... // Check if the device can evaluate the policy. if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) { [context .evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: { (success: Bool, evalPolicyError: NSError?) -> Void in })] } }
evaluatePolicy接受三個參數,第三個參數是一個完全的句柄塊。在認證成功的情況下,我們將會從磁盤加載筆記(我們將會稍後做它)。如果發生任何錯誤,它將必須被處理。實際上這僅僅是一個教程而不是一個真正的應用,因此我們打算顯示一些錯誤的信息。注意在那些可能的錯誤中,有用戶回到自定義認證的選項,並避免掃描手指,因此在比較合適的時候,我們將會調用另一個方法,這個方法是我們稍後實現,它會展示一個自定義警示視圖用來允許用戶輸入他們的密碼。
func authenticateUser() { ... // Check if the device can evaluate the policy. if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) { [context .evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: { (success: Bool, evalPolicyError: NSError?) -> Void in if success { } else{ // If authentication failed then show a message to the console with a short description. // In case that the error is a user fallback, then show the password alert view. println(evalPolicyError?.localizedDescription) switch evalPolicyError!.code { case LAError.SystemCancel.toRaw(): println("Authentication was cancelled by the system") case LAError.UserCancel.toRaw(): println("Authentication was cancelled by the user") case LAError.UserFallback.toRaw(): println("User selected to enter custom password") NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in self.showPasswordAlert() }) default: println("Authentication failed") NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in self.showPasswordAlert() }) } } })] } }
如果完成句柄的成功的參數是真,那麼我們將會加載筆記數據。然而,如果沒有錯誤。那麼我們做兩件事:第一,我們將錯誤的描述展示到控制台。evalPolicyError參數值是可選的,因此問號在錯誤值拆箱的時候是需要的。在一個switch語句中,我們檢查所有的可能錯誤情況(如果你想要,你可以使用一個if語句)。
有兩個事實需要被提及:第一不是所有的錯誤類型都在這裡,像它們中的有些可能發生在請求Local Authentication框架的時候(如果Touch ID能夠使用canEvaluatePolicy方法被提交),接下來我們將會面臨它們。第二個是我們伴隨著每一種錯誤類型使用toRaw()方法,因為我們想要每一個錯誤類型從一個枚舉類型轉換成一個原始整型。如果我們不能使用它,編譯器將會產生一個錯誤(可以隨意試試)。
除了上邊提到的,你看到的一個對showPasswordAlert方法的調用,這是一個不存在的方法並且我們稍後要實現它。像你推斷的那樣,當它被調用時,輸入密碼的自定義警示框會顯示出來。注意我們在兩種情況下調用它:當用戶決定回調到自定義密碼入口時,以及Touch ID機制不能識別用戶手指時導致認證失敗時。重點是我們只想這個方法是在主線程中,因為警示視圖的顯示意味著app在視覺上的更改,並且不能在一個輔助線程中執行,其在完成句柄中被執行。無論如何,在另外兩種情況中,我們僅僅顯示消息給控制台。
以上實現或多或少是應用程序集成Touch ID認證系統所需要的,但不能就此結束,因為我們沒有處理Touch ID警示框不能顯示的情況。在特定的情況下可能會發生:
TouchID不可用
密碼沒有被設置到設備的Settings選項中
沒有使用Touch ID錄入指紋
設備不支持Touch ID
為了解決如上所述情況,我們將會增加一個else情況到初始化的if 語句中。如果你查找第一個代碼片段,初始化我們已經定義的名為error的變量,但是我們沒有做這件事。出於教學目的,我們將會確定錯誤原因並且我們將會僅僅顯示一個消息(就像之前我們做的那樣)。當然這裡我們將會調用showPasswordAlert方法,不論錯誤是什麼,當它在TouchID不能顯示時會強制顯示密碼提輸入警示框,借助於添加的else實例再次調用方法。
func authenticateUser() { // Get the local authentication context. let context = LAContext() // Declare a NSError variable. var error: NSError? // Set the reason string that will appear on the authentication alert. var reasonString = "Authentication is needed to access your notes." // Check if the device can evaluate the policy. if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) { [context .evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: { (success: Bool, evalPolicyError: NSError?) -> Void in if success { } else{ // If authentication failed then show a message to the console with a short description. // In case that the error is a user fallback, then show the password alert view. println(evalPolicyError?.localizedDescription) switch evalPolicyError!.code { case LAError.SystemCancel.toRaw(): println("Authentication was cancelled by the system") case LAError.UserCancel.toRaw(): println("Authentication was cancelled by the user") case LAError.UserFallback.toRaw(): println("User selected to enter custom password") self.showPasswordAlert() default: println("Authentication failed") self.showPasswordAlert() } } })] } else{ // If the security policy cannot be evaluated then show a short message depending on the error. switch error!.code{ case LAError.TouchIDNotEnrolled.toRaw(): println("TouchID is not enrolled") case LAError.PasscodeNotSet.toRaw(): println("A passcode has not been set") default: // The LAError.TouchIDNotAvailable case. println("TouchID not available") } // Optionally the error description can be displayed on the console. println(error?.localizedDescription) // Show the custom alert view to allow users to enter the password. self.showPasswordAlert() } }
我相信沒有特別的困難需要討論。如我所說過的,不論錯誤是什麼,我們可以調用showPasswordAlert方法允許用戶輸入他們的密碼。
這是如何使用Swift將Touch ID認證機制集成到一個應用中。你僅僅必須要做的是,在每一種情況中編寫相關代碼以及你所有的設置。在我們繼續之前,不要忘記我們必須調用這個函數,因此進入viewDidLoad方法做這件事。
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. authenticateUser() }
注意:當你執行方法調用時,在Swift中,self關鍵字可以被忽略。然而這不可能發生在blocks裡邊,這就是為什麼在完成句柄塊中我們使用self。
現在我們有了認證機制,現在我們可以實現警示視圖來輸入密碼。
提供自定義認證方式
之前我們調用了三次showPasswordAlert方法。現在,Xcode指出有一些代碼錯誤,因為尚未定義這種方法。像之前說的那樣,並且為了讓事情保持簡單,我們不會創建精確的視圖控制器允許用戶輸入密碼作為認證的替代方案,相反我們將會顯示一個有安全文本框的提示視圖。
showPasswordAlert()變的非常簡單,我們僅僅需要顯示對話消息。如下:
func showPasswordAlert() { var passwordAlert : UIAlertView = UIAlertView(title: "TouchIDDemo", message: "Please type your password", delegate: self, cancelButtonTitle: "Cancel", otherButtonTitles: "Okay") passwordAlert.alertViewStyle = UIAlertViewStyle.SecureTextInput passwordAlert.show() }
現在每一次用戶選擇這種認證方式,或者Touch ID認證失敗,系統都會出現警示視圖。不過通過這種方法使用app還不夠。我們也必須要做的是檢查輸入的密碼是否是正確的。為了做這個,我們必須實現alertView(alertView:, clickedButtonAtIndex:)警示視圖代理方法。如果你仔細看上面的代碼,我們設定我們的類(self)作為提示視圖的代理。
我們猜測用戶密碼是appcoda字符。使用一個設置密碼的表格來實現一個精確的視圖控制器是毫無意義的。如你將會見到的,如果密碼不正確,或者如果用戶沒有輸入任何密碼,我們會再次顯示警示視圖。
func alertView(alertView: UIAlertView!, clickedButtonAtIndex buttonIndex: Int) { if buttonIndex == 1 { if !alertView.textFieldAtIndex(0)!.text.isEmpty { if alertView.textFieldAtIndex(0)!.text == "appcoda" { } else{ showPasswordAlert() } } else{ showPasswordAlert() } } }
在給定的密碼是正確的情況下,我們會僅加載筆記數據並且我們會將其展示給tableview。
現在Xcode產生一個錯誤,因為我們沒有使用UIAlertViewDelegate協議。這很容易解決,你僅僅需要到文件頂部,然後將其添加到UIViewController的父類。
class ViewController: UIViewController, UIAlertViewDelegate
注意:當進行子類化並且遵照協議時,父類首先被寫,然後是你需要的所有協議,並且用逗號將它們分割。
備用認證機制已經准備好了。設備不使用警示視圖來請求密碼時是一個糟糕的想法。在演示中沒有多大關系,但是在真實的世界中…是有關系的。
創建一條新筆記
現在我們已經執行了所有可能的方式用來認證用戶並且使用app,我們能夠通過實現app自身來繼續前進。我們將會通過構建新筆記來開始,這在我們顯示任何筆記到tableview之前是需要的。如果我們不能建立數據,就不能顯示數據。
教程開始時,我已經提到了幾次,筆記數據是被存儲到磁盤上,路徑是app下的 documents目錄下。從編程角度來說,那就意味著我們必須開發必須的方法用來得到筆記文件的存儲路徑,以及檢查文件是否存在。這兩種功能在兩種情況下是需要的:在ViewController類檢查是否文件存在以及便於加載數據,還有在EditNoteViewController類中,為了加載任何已存在的數據以及追加新的數據,當然還有保存被編輯的筆記。
由於我們在兩個不同的類中做的事情大部分相同,所以我們會在AppDelegate類中實現兩個方法並且實例化一個應用程序代理對象,我們將會直接使用它們。第一個方法將會返回筆記文件的全路徑。進入AppDelegate.swift 文件,並增加下一個實現:
func getPathOfDataFile() -> String { let pathsArray = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true) let documentsPath = pathsArray[0] as String let dataFilePath = documentsPath.stringByAppendingPathComponent("notesData") return dataFilePath }
像你看到的,我將筆記文件命名為"notesData",但是實際上不論你起什麼名字都無所謂。在以上實現中,它演示我們如何能夠在Swift中直接使用文檔。這是有用的,你可以保留它作為一個小的可重用的代碼片段。除此,這是我們第一次編寫方法返回一個值,並且在這種情況中返回的是一個字符串。當調用這個方法時,會返回全路徑,因此我們不需要手動合成路徑。
現在,讓我們寫一個方法來檢查文件是否存在於文檔中:
func checkIfDataFileExists() -> Bool { if NSFileManager.defaultManager().fileExistsAtPath(getPathOfDataFile()) { return true } return false }
這是極其簡單的!在這,我們使用NSFileManager類來判斷文件是否存在,並且僅僅像Objective-C中做的那樣。如果文件被找到,我們返回真,否則返回假。
借助工具盒中這兩個便利的方法,我們能夠繼續筆記建立。進入EditNoteViewController.swift文件,並且同時聲明和初始化應用代理常量。
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
你可以在最後的IBOutlet方法之後寫如上代碼。注意我們使用關鍵字轉換我們分配給應用代理的常量。
現在,為了便於生成一個新的筆記我們需要做什麼呢?答案很簡單:讓保存按鈕正常工作。但在那之前,我們先做些其他事情。
如果鍵盤在這個視圖控制器被推入到導航棧中的時候顯示一次,將是一件不錯的事情。那種方式對於我們來說,寫筆記將會容易的多。它僅僅需要寫一行代碼,並且必須要被增加到viewDidLoad方法中:
self.txtNoteTitle.becomeFirstResponder()
借助於此,每次視圖控制器被加載時,textfield將會引起關注並且會顯示鍵盤。還有就是,如果我們讓文本視圖在文本框鍵盤被輕擊返回鍵的時候,成為第一響應者將會是很酷的。為了這個目的,我們需要做三件事情:遵守UITextFieldDelegate協議,讓我們的類成為文本域的代理,並且最終實現一個文本域的代理方法控制返回鍵的行為。
我們看到用Swift編寫代碼是更好的選擇。首先遵照必須的協議:
class EditNoteViewController: UIViewController, UITextFieldDelegate
接下來,讓我們把這個類變成文本域的代理,這將會發生在viewDidLoad中:
txtNoteTitle.delegate = self
最後,textfield代理方法如下:
func textFieldShouldReturn(textField: UITextField!) -> Bool { // Resign the textfield from first responder. textField.resignFirstResponder() // Make the textview the first responder. tvNoteBody.becomeFirstResponder() return true }
正如你在以上代碼中看到的,當鍵盤的返回鍵被輕擊的時候,我們從第一響應者中放棄textfield,並且textview會獲得焦點。
現在讓我們返回到保存按鈕,並且讓我們聚焦到我們如何讓它工作上邊來。首先,如你所知我們需要建立一個IBAction方法來觸發保存動作。需要打開Main.storyboard文件,並且一旦出現Interface Builder,則打開輔助編輯器。
確保ViewController.swift文件在輔助編輯器上:
現在在保存按鈕上點Ctrl-按鈕,並且拖拽到輔助編輯器上:
在顯示出的對話框上,在Connection下拉菜單中選擇Action選項,並設定saveNote值作為IBAction方法的名字。然後點擊聯接按鈕。
現在你可以關閉輔助編輯器,並且返回到EditNoteViewController.swift文件。
我們將要在saveNote方法做的第一件事情是檢查用戶是否已經輸入一個標題。如果沒有標題,那麼我們將會什麼也做不了,我們將會從那個方法返回:
@IBAction func saveNote(sender: AnyObject) { if self.txtNoteTitle.text.isEmpty { println("No title for the note was typed.") return } }
我們將會遵循如下邏輯:
首先,我們將會設定一個字典對象(Swift 字典)標題和note body值。
接下來,我們將會定義一個可變的數組(NSMutableArray).
如果筆記數據文件已經存在,那麼我們將會初始化以上數組,並且我們將會追加新的字典到那個數組中。
如果筆記數據不存在,我們將會通過增加字典到初始化方法中來簡單初始化數組
我們將會存儲文件到磁盤
我們將會從導航控制器棧彈出視圖控制器
所有以上用如下代碼解釋:
@IBAction func saveNote(sender: AnyObject) { if self.txtNoteTitle.text.isEmpty { println("No title for the note was typed.") return } // Create a dictionary with the note data. var noteDict = ["title": self.txtNoteTitle.text, "body": self.tvNoteBody.text] // Declare a NSMutableArray object. var dataArray: NSMutableArray // If the notes data file exists then load its contents and add the new note data too, otherwise // just initialize the dataArray array and add the new note data. if appDelegate.checkIfDataFileExists() { // Load any existing notes. dataArray = NSMutableArray(contentsOfFile: appDelegate.getPathOfDataFile()) // Add the dictionary to the array. dataArray.addObject(noteDict) } else{ // Create a new mutable array and add the noteDict object to it. dataArray = NSMutableArray(object: noteDict) } // Save the array contents to file. dataArray.writeToFile(appDelegate.getPathOfDataFile(), atomically: true) // Pop the view controller self.navigationController!.popViewControllerAnimated(true) }
就這麼簡單!注意現在兩個方法很方便的在應用程序中實現。相同的用途也將在之後也被改進。
如上實現是很棒的,保存按鈕每次被輕擊,筆記就會被保存到磁盤上並且彈出視圖控制器將會。然而,有一個注意的問題: ViewController類不能知道一個筆記在EditNoteViewController視圖控制器被彈出的時候是否已經被保存,並且它不會更新表視圖。這是一個系列事件,並且我們將會逆向攻擊它,通過實現一個自定義的協議和使用代理模式來做到。我們稍後做,像我們必須首先返回到ViewController類並且實現數據加載的特征。
筆記列表
在Touch ID和自定義驗證實現過程中,我提到筆記數據將會在特定情況加載,但是我們仍然什麼也沒做。現在我們能夠成功創建筆記,我們能夠實現數據加載功能並且列出已經存在的筆記到tableview中。
從ViewController.swift f文件開始。這裡,我們必須完成兩個特定的任務:第一個是建立一個新方法用於加載數據。第二個是實現所有必須實現的tableview方法,這樣我們可以適當地顯示加載方法到ableview中。
我們將會從第一個開始,我們將會寫一個新的方法,命名為loadData。在我們這麼做以前,我們需要兩件事情。首先,我們必須實例化應用代理對象,因此我們能夠使用我們早期實現的應用代理的兩個方法。回到頂部類的IBOutlet屬性後面,寫下面的代碼:
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
還有就是,我們需要一個數組(一個NSMutableArray),它將會被用來包裝數據。如果你想要知道為什麼是一個可變的數組而不是一個固定的,那麼我必須說因為之後我們將會實現另一個特征用來刪除筆記,並且我們將會需要改變數組的內容。證像如下app 代理中定義的那樣,增加這個:
var dataArray: NSMutableArray!
注意數組已經被定義為一個可空項,因為如果沒有數據文件存在,數組將會保留nil.
現在,我們能夠繼續新方法的實現。像你將會看到的那樣,它比較簡單。如果數據文件存在,那麼我們加載它的內容到數組中並且我們重新加載了tableview,否則我們僅僅顯示一個消息到控制台。
func loadData(){ if appDelegate.checkIfDataFileExists() { self.dataArray = NSMutableArray(contentsOfFile: appDelegate.getPathOfDataFile()) self.tblNotes.reloadData() } else{ println("File does not exist") } }
通過以上方法的准備,我們能夠去調用它。讓我們從authenticateUser方法開始,在完成句柄塊中和成功驗證情況中:
if success { NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in self.loadData() }) }
再一次,我必須強調需要用主線程加載並顯示數據。
還有,讓我們在密碼被正確輸入到警示視圖時調用它:
if alertView.textFieldAtIndex(0).text == "appcoda" { loadData() }
讓我們現在到這部分的第二個任務中。這要設置tableview屬性,因此我們可以羅列我們從文件加載的筆記。
初始化,我們必須遵照UITableViewDelegate 和 UITableViewDataSource協議,因此到文件頂部並增加它們:
class ViewController: UIViewController, UIAlertViewDelegate, UITableViewDelegate, UITableViewDataSource
當然,我們不要忘記我們自己的類必須是tableview代理或者數據源。進入到viewDidLoad方法:
override func viewDidLoad() { ... tblNotes.delegate = self tblNotes.dataSource = self }
最後,讓我們開始必須的tableview的代理和數據源方法的實現。首先,我們必須指定tableview有多少個部分:
func numberOfSectionsInTableView(tableView: UITableView!) -> Int { return 1 }
接下來,我們必須返回適當的行數。記住如果筆記數據文件不存在,dataArray可變數組將會初始化並且它將會保留nil。所以,我們必須首先確認數組不是nil然後返回適當的行數,否則我們必須返回0.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let array = dataArray { return array.count } else{ return 0 } }
像你看到的那樣,如果dataArray實際存在,我們解封它到數組常量中並且我們返回對象總數。
在繼續進行之前,我必須說有一個替代方案對於以上實現來說。實際上按照如下代碼可返回對象總數:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataArray.count }
只要你不定義dataArray作為一個可空項,但是你要在定義之後初始化它,因此它不能為空。所以,不要寫這樣的內容…
var dataArray: NSMutableArray!
你需要這麼寫:
var dataArray: NSMutableArray = NSMutableArray()
然而,這種方式將會讓dataArray使用文件內容被再初始化一次,這個過程在loadData方法中做,但是此外,我們的初始化實現是一個使用可空項的不錯的實現,我們的下一步是返回一個cell:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("idCell") as UITableViewCell let currentNote = self.dataArray.objectAtIndex(indexPath.row) as Dictionary cell.textLabel!.text = currentNote["title"] return cell }
在移除cell之後,我們分配字典(該字典中有每一個筆記數據)給currentNote常量。接著,我們就能得到筆記的標題,然後我們把標題設置到下一個cell的標簽上。
最後,我們需要一個方法便於指出每一行的高度:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return 60.0 }
現在,任何存在於文件中的筆記都可以在表視圖中列出來。接著,我們將會看到如實現代理屬性,所以表視圖在一個新的筆記生成的時候被重新加載。
代理模式
如果在Objective-C中必須使用代理模式,從而將其他不同類中的數據改變通知代理類,在這個時候,便有特定的步驟是需要做的。這些步驟包括一個協議的建立,特定方法的定義,代理類和新的協議的一致性等等。在Swift中,使用代理並不困難,和Objective-C中有很多相似。
我在本教程的前面部分曾經說過,我們必須在新筆記建立的時候告訴ViewController類(或者在我們接下來將會看到的更新的時候),這樣它會重磁盤中重新加載數據並刷新tableview,這樣的事情不會很簡單,因為EditNoteViewController類不通知ViewController類,因此使用代理模式是最有必要的。
第一個步驟是建立一個新的協議,打開EditNoteViewController.swift文件,並且到該文件頂部,也就是類的實現部分之前。然後,增加協議:
protocol EditNoteViewControllerDelegate{ }
在那裡,我們將會只定義一個方法,該方法將會在一個筆記被保存的時候調用:
protocol EditNoteViewControllerDelegate{ func noteWasSaved() }
接著,我們必須定義一個代理屬性(變量)。這次是在類內部,寫如下代碼:
var delegate : EditNoteViewControllerDelegate?
注意在上面命令的末尾標記的問題。代理屬性必須是一個可選的值,因為可能沒有對象分配給它(比如,舉個例子,我們不設定任何代理類),因此它會保留空值。
現在有兩個重要的任務是需要我們完成的:首先,要更新saveNote IBAction方法,因此當一個筆記被保存時noteWasSaved代理方法就會被調用。第二點,要在ViewController類中實現這個方法,因為每次它要接收消息加載數據並且更新表視圖。
從第一點開始,進入到saveNote IBAction方法並在從導航堆棧彈出視圖控制器之前,增加如下代碼:
@IBAction func saveNote(sender: AnyObject) {
…
// Notify the delegate class that the note has been saved. delegate?.noteWasSaved() // Pop the view controller self.navigationController!.popViewControllerAnimated(true) }
為了便於代理類知道筆記被保存,我們所需要知道的所有的都在這裡了。
現在,讓我們返回到ViewController類。首先,需要遵守新的協議,因此現在做這麼一件事:
class ViewController: UIViewController, UIAlertViewDelegate, UITableViewDelegate, UITableViewDataSource, EditNoteViewControllerDelegate
通過添加如上協議,Xcode會產生一個錯誤,告訴我們ViewController類沒有遵守該協議。這個錯誤的發生是因為我們沒有實現noteWasSaved代理方法。如下:
func noteWasSaved() { // Load the data and reload the table view. loadData() }
loadData方法將會做所有我們需要的事情。它將從磁盤加載筆記數據並且它會刷新表視圖。
還有一件我們必須最後做的事情。那就是讓ViewController 類成為EditNoteViewController的代理。我們將會在prepareForSegue方法中這樣做,因為當使用segues時,它是適當的地方,如下:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) { // Get the new view controller using segue.destinationViewController. // Pass the selected object to the new view controller. if segue.identifier == "idSegueEditNote"{ var editNoteViewController : EditNoteViewController = segue.destinationViewController as EditNoteViewController editNoteViewController.delegate = self }
在上面的代碼中沒有其他的segue和if 語句可以被忽略,我在想要展示出如何在Swift中檢查目的segue有意增加它。無論如何,使用destinationViewController屬性,我們得到一個EditNoteViewController的實例,然後設定我們的類作為它的代理。
這就是所有的了!如果你回顧我們剛剛做的事情,你將會發現在Swift中使用協議和代理是容易的,在這個過程中涉及到的步驟是非常特別的。
編輯筆記
在這個點上,我們的演示程序是完全具備功能,此外,也是完全受保護的。然而,讓它做更多的事情是非常有意思的,所以我們有機會知道更多關於Swift。因此,我們將會增加兩個特征分別是編輯已存在的筆記和被刪除的。
在這個部分,我們將會把精力集中到我們將如何編輯一個筆記。我們遵照的邏輯是非常簡單:一旦用戶在一個表視圖cell上輕擊想要編輯一個筆記,被輕擊的行的索引將會被發送給EditNoteViewController視圖控制器。這個視圖控制器將會從磁盤加載筆記,並且它將會顯示匹配被接收的筆記的細節。然而,我們不應該忘記更新saveNote方法,所以要保存一個被編輯的筆記作為一條新的。
我們將會一步步看到每件事情。首先,我們必須實現下一個表視圖方法用來完成在cell上的輕擊。
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { }
當一個cell被輕擊,我們想要保持它的行索引然後通過完成相應的segue來顯示EditNoteViewController視圖控制器。我們必須定義一個新的屬性用來存儲行索引,所以在類頂部,增加下面這行。
var noteIndexToEdit: Int!
在新的表視圖方法中,有我們要做的事情:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { noteIndexToEdit = indexPath.row performSegueWithIdentifier("idSegueEditNote", sender: self) }
都不難,我們只要存儲行索引並指定它的標識完成轉場就可以了。
依據我之前所說,noteIndexToEdit屬性的值一旦加載我們就必須發送給EditNoteViewController。從編程角度,這也就意味著我們必須建立一個近似於EditNoteViewController的屬性,並給它分配noteIndexToEdit屬性的值。
打開EditNoteViewController.swift文件,定義下面這個:
var indexOfEditedNote : Int!
現在,返回到ViewController.swift文件並定位到prepareForSegue方法。在那個方法中,做我們之前描述過的事情。
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) { if segue.identifier == "idSegueEditNote"{ ... if (noteIndexToEdit != nil) { editNoteViewController.indexOfEditedNote = noteIndexToEdit noteIndexToEdit = nil } } }
現在有一個重要的觀察。注意在我們將要編輯的編輯的索引分配給indexOfEditedNote屬性之後,我們讓noteIndexToEdit為空。如果我們想要在建立新的筆記,這麼做是有必要的。如果我們不這麼做,那麼在編輯一個筆記之後,noteIndexToEdit屬性仍然會有一個值,而且在建立一個新的筆記的時候,EditNoteViewController視圖控制器會認為我們想要編輯一個已經存在的筆記。當然,那樣會引發一個嚴重的問題。
在這個時候,我們已經完成了我們在這個類中必須要做的。讓我們現在再次打開EditNoteViewController.swift,我們要在這裡面做些事情。首先,為了便於讓一些事情明確,讓我們建立一個建立一個新方法:
func editNote() { // Load all notes. var notesArray: NSArray = NSArray(contentsOfFile: appDelegate.getPathOfDataFile()) // Get the dictionary at the specified index. let noteDict : Dictionary = notesArray.objectAtIndex(indexOfEditedNote) as Dictionary // Set the textfield text. txtNoteTitle.text = noteDict["title"] // Set the textview text. tvNoteBody.text = noteDict["body"] }
如你看到的,我們完成了一些特定的步驟。首先我們從磁盤加載所有的筆記,然後我們分配一個字典對象給我們要編輯的筆記,最後在我們可以改變它們的時候,我們設定文本域和文本視圖的內容。
准備好以上方法,我們必須找到要調用它的地方。選擇一個正確的時間點調用是重要的,因為textfield和textview都不可能在我們調用方法的時候被初始化。例如,如果我們在viewDidLoad方法中調用這個方法,那麼我們的app有可能崩潰,因為文本域和文表示圖仍然為空。
最好的地方是,我們可以知道我們的這些子視圖在視圖顯示之後已經被初始化,所以我們只需要覆蓋並實現viewDidAppear方法。如下:
override func viewDidAppear(animated: Bool) { if (indexOfEditedNote != nil) { editNote() } }
注意在我們調用我們的方法之前,我們要檢查indexOfEditedNote是否有一個實際的值或者是空。
最後,我們必須再一次稍微修改下saveNote IBAction方法,以便於能夠保存一個被編輯的筆記。如你所料,我們將要檢查indexOfEditedNote屬性是否有一個值還是為空。如果有一個值,我們將會用正在便捷地值替代已有的,如果為空,我們只會保存新值。
進入saveNote IBAction方法,然後找到如下if語句:
if appDelegate.checkIfDataFileExists
在其方法體中,我們將會完成我之前說過的事情。如下修改:
@IBAction func saveNote(sender: AnyObject) { ... if appDelegate.checkIfDataFileExists() { // Load any existing notes. dataArray = NSMutableArray(contentsOfFile: appDelegate.getPathOfDataFile()) // Check if is editing a note or not. if indexOfEditedNote == nil { // Add the dictionary to the array. dataArray.addObject(noteDict) } else{ // Replace the existing dictionary to the array. dataArray.replaceObjectAtIndex(indexOfEditedNote, withObject: noteDict) } } ... }
如果indexOfEditedNote屬性為空,那麼我們只要增加有內容的筆記到dataArray數組即可。然而,如果那個屬性有一個值,那麼我們將已有的字典替換成數組,該數組有一個被屬性指定的索引。
現在我們的應用程序有能力編輯和保存已有的筆記了!
刪除筆記
我們已經看到許多不同的事情,並且現在到了要實現最後一個的時候了:筆記刪除。由於增加這個功能,我們的例子將會變得盡可能的完善。
首先,打開ViewController.swift文件。這裡,我們將會實現如下表視圖方法:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { }
使用if 語句,我們將能確定用戶是否用手指向左滑動cell或者為了展示刪除按鈕。如果是那種情況,我們將會從數據源數組( dataArray)中移除適當的字典,並且我們將會將數組的內容儲存至文件從而保證其更新,最後我們用動態的方式刷新tableview:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == UITableViewCellEditingStyle.Delete{ // Delete the respective object from the dataArray array. dataArray.removeObjectAtIndex(indexPath.row) // Save the array to disk. let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate dataArray.writeToFile(appDelegate.getPathOfDataFile(), atomically: true) // Reload the tableview. tblNotes.reloadSections(NSIndexSet(index: 0), withRowAnimation: UITableViewRowAnimation.Automatic) } }
這就是我們需要的一切!任何已有的筆記現在可以被直接刪除了。增加一個確認提示視圖可能是有用的,但是這會變得多余。
編輯和運行
雖然我確定你已經運行和體驗這個app了,但我有責任說是時候嘗試它了。運行app並使用TouchID,自定義的警示視圖,當然了,還要添加、編輯和刪除筆記。下面是該app的一些截圖:
TouchID對話框
有輸入密碼時安全文本域的提示視圖
編寫一條新筆記
顯示筆記
刪除一條筆記
概要
現在只是關於iOS 8的第一篇教程結束了。由於是第一次,我們建立一個應用程序使用了大量Swift 編程語言,並且我們用它寫了很多代碼。我有目的性地示范了許多應用的例子,對於學習許多新的事情很有幫助。
在我完成教程前,我將會有一個快速的回顧。我們用Swift 做了如下事情:
實現Touch ID 驗證機制並使用LocalAuthentication 框架。
創建了IBOutlet 屬性和IBAction 方法。
實現了所需的tableview方法。
建立了一個簡單的協議。
使用了代理模式。
使用了文件和文檔路徑
在主線程用NSOperationQueue 類執行一個方法。
把一個簡單的想法轉化成一個應用的邏輯
希望本文能對你有所幫助。為了方便學習參考,你可以下載完整的Xcode工程代碼。