在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工程代码。
来源:http://www.cocoachina.com/ios/20141114/10223.html