美文网首页swiftiOS Developer
iOS Apprentice中文版-从0开始学iOS开发-第二十

iOS Apprentice中文版-从0开始学iOS开发-第二十

作者: Billionfan | 来源:发表于2017-06-19 02:02 被阅读316次

    制作table view cell的几种方法

    在AllListsViewController中创建table view cell的方法比在ChecklistViewController中略复杂一些。在后者中你仅仅是通过简单的一个语句就获得了一个新的table view cell:

    let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem",for: indexPath)
    

    但是在AllListsViewController中为了实现同样的目的我们写了一大堆代码:

    let cellIdentifier = "Cell"
    if let cell =
           tableView.dequeueReusableCell(withIdentifier: cellIdentifier) {
      return cell
    } else {
      return UITableViewCell(style: .default,reuseIdentifier: cellIdentifier)
    }
    

    这里我们还是调用了dequeueReusableCell(withIdentifier),只是以前我们在故事模版中放置了cell并且给了它一个身份标示,而这次没有。

    如果这个table view找不到任何可重用的cell,这个方法会返回nil,这时你不得不手动创建cell,这就是else后面跟的代码的作用。

    这实际上是两种不同类型的dequeueReusableCell(...),其中一个有IndexPath参数而另一个没有。在AllListsViewController中我们使用的是没有IndexPath参数的这一个。两者的区别在于有IndexPath参数的这一个仅用于标准cell。如果在AllListsViewController中使用有IndexPath参数的这个方法,app就会崩溃掉。

    制作cell有四种方法:

    1、使用标准cell。这是最简单也是最快的一种方法。我们在ChecklistViewController中做的就是。

    2、使用静态cell。你在Add/Edit界面中使用的就是静态cell。静态cell最大的优势就是不用给它提供数据源方法,适用于你提前知道cell内容的情况。

    3、使用nib文件。一个nib(也叫做XIB)就像一个迷你的仅仅包含一个自定义的UITableViewCell对象的故事模版。这和使用标准cell非常相似,只是你是在故事模版之外使用它。

    4、手动创建,就是我们在AllListsViewController中使用的方法。在早期的iOS版本中,只有这一种方法。这种方法要复杂一些,但是更加灵活。

    当你手动创建一个cell时,你需要指定一个确定的cell style,就会得到一个已经包含标签和图片的预置布局的cell。

    在All Lists View Controller中,你使用了“Default”style,在稍后你会将它切换为“Subtitle”,这会在主标签的下方,给你一个小一点的次级标签。

    使用标准cell style意味着你不需要设计你自己的cell布局。对于大多数app而言标准cell已经足够用了。

    标准cell和静态cell都可以使用标准cell style。标准cell和静态cell的默认style都是“Custom”,这种style要求你使用自己的标签,但是你可以通过界面建造器将它改变为内建的style。

    最后,你需要注意的是:有时我看到其他人是这样写代码的,使用代码为每一行创建一个新的cell而不是试着重用cell。你千万不要这样做!一定要首先向table view请求看看是否有可以重用的cell,使用dequeueReusableCell(...)这个方法。

    为每一行都创建一个新的cell,会使app变慢,创建一个对象总是比重用一个对象要慢。所以为每一行都创建一个新的cell会占据大量内存,为了用户着想,你也应该重用cell。

    查看待办事项分类

    目前,由AllListsViewController中的lists数组组成的数据模型包含了少量的Checklist对象。数据模型中同时还有来自ChecklistViewController的items数组,其中包含ChecklistItem对象。

    你也许已经注意到了,当你点击任何一行时,无论是哪一行,都会展示一模一样的待办事项。

    而实际上,每个待办事项分类,都应该对应不同的待办事项内容。我们之后会完成这一工作。

    首先,我们来设置好映射被选择的待办事项分类的名称,作为界面的标题。

    打开ChecklistViewController.swift,添加一个实例变量:

    var checklist: Checklist!
    

    过会我再讲为什么这必须是个可选型。

    还是在ChecklistViewController.swift中,将viewDidLoad()方法修改为:

    override func viewDidLoad() {
            super.viewDidLoad()
            title = checklist.name
        }
    

    这一步的作用是改变界面的标题,就是导航栏的标题,将导航栏的标题修改为Checklist对象的名称。

    当执行转场时,你会将这个checklist对象给到ChecklistViewController。

    打开AllListViewController.swift,将tableView(didSelectRowAt)修改为:

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let checklist = lists[indexPath.row]
            performSegue(withIdentifier: "ShowChecklist", sender: checklist)
        }
    

    和以前一样,你使用performSegue()来执行转场。这个方法之前有一个参数sender,之前是nil。现在你用来传递用户点击的那一行的Checklist对象。

    你可以在sender参数中放置任何东西。如果你通过故事模版执行转场(而不是像现在这样手动转场),那么sender就会引用被触发的空控件,例如用于Add按钮的UIBarButton对象或者用于列表中某一行的UITableViewCell。

    但是因为你是通过手动开始转场的,所以你可以在sender中放入最方便的对象。

    将Checklist对象放入sender参数时,还不会将这个对象给到ChecklistViewController。这一步发生在“prepare-for-segue”中,你还没有在代码里写这个方法。

    在AllListsViewController.swift中添加以下方法:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "ShowChecklist" {
                let controller = segue.destination as! ChecklistViewController
                controller.checklist = sender as! Checklist
            }
        }
    

    你之前应该见到过这个方法。prepare(for:sender:)在转场执行后立即被调用。你可以在这里,在新的视图还没有在屏幕上可视化之前设置新视图的属性。

    ⚠️:转场的目标是ChecklistVieController,不是UINavigationController,这和之前有点不同。
    到Add/edit界面的转场是一种modally presented(这个真心不知道怎么翻译)方式,针对与嵌入导航控制器中的视图控制器。
    而这次是“Push”型的转场,直接转到Checklist View Controller。
    看看故事模版就知道在All Lists界面和Checklist界面之间没有导航控制器。这个转场直接从一个视图转到另一个。

    在prepare(for:sender:)中,你需要将被点击行的Checklist对象给到ChecklistViewController。这就是为什么之前你将Checklist对象放入sender参数中的原因。(你也可以将Checklist对象临时存储到一个实例变量里,但是把这个对象放入sender参数中更加简单)

    所有这一切发生在ChecklistViewController被加载前,ChecklistViewController被实例化时的一瞬间。这就是说它的viewDidLoad()方法在prepare(for:sender:)之后被调用。

    在这一时刻,这个视图控制器的checklist属性被来自sender的Checklist对象填充,并且viewDidLoad()可以据此修改界面的标题。

    涉及转场的步骤

    这一系列过程解释了为什么checklist属性被声明为可选型。因为直到调用viewload()前,它都是nil。

    nil通常不是Swift中允许的变量取值,但是可选型例外。

    之前我们声明可选型时用的是问号,这里是一个感叹号,感叹号的作用和问号非常类似,区别在于用感叹号时,你不需要用if let去对它进行解包。

    使用这种隐式解包可选型时,需要非常小心,因为它们没有任何保护措施。

    运行app,点击一个待办事型分类,转入的屏幕界面的标题会显示为这个待办事项分类的名称。

    注意一点,把Checklist对象给到ChecklistViewController并不会形成一个拷贝。

    你仅仅是传递这个对象的一个引用到视图控制器,用户对Checklist对象做出的任何变更,都会体现在AllListsViewController上。

    这两个视图控制器读取的都是同一个Checklist对象。过会在Checklist中添加新的ChecklistItem时,这一点会成为你的便利条件。

    类型扮演(type cast)

    在prepare(for:sender:)中,你写了这样的代码:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            ...
                controller.checklist = sender as! Checklist
                ...
        }
    

    这里的as!是什么呢?

    如果你足够细心的话,你会注意到“as something”已经出现过好几次了。这就是类型扮演(type cast)

    类型扮演通知Swift解释具有不同数据类型的值。这和电影中的某一个演员正好相反,在电影中一个演员只扮演一个角色,而在swift中,类型扮演的实际作用就是改变了对象的角色。

    上面的方法中,sender的参数是Any?,这意味着这个参数可以是任何类型的对象:一个UIBarButtonItem,一个UITableViewCell,或者一个Checklist对象。感谢这里的问号,使得它甚至可以为nil。

    但是controller.checklist总是期待一个合适的Checklist对象,它无法处理其他对象,比如UITableViewController,因此,swift需要你只能把Checklist对象放入checklist属性中。

    通过“sender as! Checklist”,你告诉了Swift它可以安全的将sender作为Checklist对象处理。

    另一个类型扮演的例子是:

    let controller = segue.destination as! ChecklistViewController
    

    转场的destination(目的地)属性引用转场结束时接受到的视图控制器,显然 ,苹果的工程师无法提前预言这个视图控制器就是我们命名的ChecklistViewController。

    所以你不得不在读取任何这个对象的属性前,先将它由通用类型UIViewController扮演为这个app中存在的ChecklistViewController。

    在举一个例子,在loadChecklistItems()中:

     items = unarchiver.decodeObjectForKey("ChecklistItems")
                                                      as! [ChecklistIt]
    

    NSKeyedUnarchiver将"ChecklistItems"键值下冻结的对象解码到一个数组中,但是你必须告诉swift这确实是一个包含ChecklistItem对象的数组。

    没有类型扮演的的话,swfit会认为这是任何类型,这样就会造成和items数组的数据类型不相容的事情发生。

    还有一种使用as?的类型扮演,这是用于可选型的类型扮演,或者说这个类型扮演可能会为nil。我们会在后面接触到这种例子。

    如果你不太理解这些内容也不要担心,我们会通过大量的例子让你消化这个内容。

    你使用类型扮演的最终原因是,iOS架构的通信原理是由Object-C写成的,swift在类型上的要求比OC要宽松一些,在OC中你需要更加精确的指明类型。

    添加和编辑待办事项分类

    让我们快速完成添加和编辑待办事项分类功能。这是另一个拥有静态cell的UITableViewController。

    如果之前的代码你已经了然于心了,那么现在工作对你就是小菜一碟!

    在工程导航器中新增一个Cocoa Touch Class模版或者直接新增一个swift文件,取名为ListDetailViewController。

    将模版中原有的内容都删掉,替换为下面的语句:

    import UIKit
    
    protocol ListDetailViewControllerDelegate: class {
        func listDetailViewControllerDidCancel(_ controller: ListDetailViewController)
        func listDetailViewController(_ controller: ListDetailViewController,didFinishAdding checklist: Checklist)
        func listDetailViewController(_ controller: ListDetailViewController,didFinishEditing checklist: Checklist)
    }
    
    class ListDetailViewController: UITableViewController,UITextFieldDelegate {
        
        @IBOutlet weak var textField: UITextField!
        @IBOutlet weak var doneBarButton: UIBarButtonItem!
        
        weak var delegate: ListDetailViewControllerDelegate?
        
        var checklistToEdit: Checklist?
        
    }
    

    我仅仅是把ItemDetailViewController.swift中的内容拷贝过来改了改名字。同时注意一下,你现在要处理的是Checklist对象,而不是ChecklistItem。

    添加一个viewDidLoad()方法:

    override func viewDidLoad() {
            super.viewDidLoad()
            
            if let checklist = checklistToEdit {
                title = "Edit Checklist"
                textField.text = checklist.name
                doneBarButton.isEnabled = true
            }
        }
    

    这样当用户编辑已经存在的待办事项分类时,可以将界面的标题修改为Edit Checklist,并且将被修改的待办事项分类的名称放入text field。

    同时也添加一个viewWillAppear()方法,用于自动弹出小键盘:

    override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            textField.becomeFirstResponder()
        }
    

    然后给Cancel按钮以及Done按钮添加动作方法:

    @IBAction func cancel() {
            delegate?.listDetailViewControllerDidCancel(self)
        }
        
        @IBAction func done() {
            if let checklist = checklistToEdit {
                checklist.name = textField.text!
                delegate?.listDetailViewController(self, didFinishEditing: checklist)
            } else {
                let checklist = Checklist(name: textField.text!)
                delegate?.listDetailViewController(self, didFinishAdding: checklist)
            }
        }
    

    这些代码对你应该非常熟悉了。这和之前的编辑及添加待办事项界面几乎一模一样。

    为了在done()方法中创建新的Checklist对象,你使用了Checklist的init(name)方法,并且将textField.text作为参数传入到name中。

    你不能像下面这样去实现这个目的,这样做是达不到预期效果的:

     let checklist = Checklist()
    checklist.name = textField.text!
    

    因为Checklist不具备一个没有任何参数的init()方法,所以Checklist()会返回一个报错。它只有一个init(name)方法,所以你每次创建一个新的Checklist对象时,都必须用这个方法进行初始化。

    同时确保用户无法选择text field所在行的cell:

    override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
            return nil
        }
    

    最后添加text field的委托方法,根据用户的输入是否为空来启用或者禁用Done按钮。

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            let oldText = textField.text! as NSString
            let newText = oldText.replacingCharacters(in: range, with: string) as NSString
            doneBarButton.isEnabled = (newText.length > 0)
            return true
        }
    

    这也是你在ItemDetailViewController中做过一次的事。

    让我们在界面建造器中为这个新的视图控制器制作用户界面。

    打开故事模版,拖拽一个Navigation Controller到画布中并且将它放置在其他视图控制器的下面。

    界面建造器已经假定你要嵌入一个table view controller到导航控制器中,这样就为你省了不少事。

    选定新的table view controller(名字叫做“root view controller”的那个)并且打开身份检查器。将class中填写为ListDetailViewController。

    将导航栏的标题由“Root View Controller”修改为Add Checklist。(如果双击不好使的话,你可以在纲要面板中选定Root View Controller然后在属性检查器中进行改名)

    添加Cancel和Done按钮并且将按钮和动作方法链接起来。同时将Done按钮和doneBarButton链接起来,并且取消选定Enable选项。

    小贴士:如果你无法将 bar button拖拽到导航栏上,也可以直接往略缩面板里拖。

    选中table view,然后在属性检查器中设置Static Cells,和style设置为Grouped。然后删除掉多余的两个cell。

    拖拽一个Text Field到cell中,然后对其进行如下配置:

    Border Style: none
    Font size: 17
    Placeholder text: Name of the List
    Adjust to Fit: disabled
    Capitalization: Sentences
    Return Key: Done
    Auto-enable Return key: check

    然后将这个Text Field和textField outlet链接起来。

    然后按住ctrl将Text Field拖拽到视图控制器上,在弹出窗口中选择delegate。这样这个视图控制器就是text field的委托了。

    打开text field的链接检查器,将Did End on Exit拖拽到代表视图控制器的黄色圆圈图标上,在弹出窗口中选择done。

    (以上步骤如果不熟悉,可以回头去看看之前的课程,这些步骤我们都详细做过一遍)

    最终的结果

    回到All Lists View Controller(就是叫做Checklists的那个),并且拖拽一个bar button上去,并且将这个button设置为Add。

    按住ctrl拖拽这个新的Add按钮到下面的导航控制器上,并且在弹出窗口选择Present Modally segue。

    选择这个新的转场,并且将其命名为AddChecklist。

    你的故事模版现在看起来应该是这个样子:

    全家福,三个导航控制和四个table view controller

    坚持一下,就快完了。你还需要将AllListsViewController做成ListDetailViewController的委托。我们之前也做过一次类似的事情。

    通过在All Lists view controller的class声明行中添加ListDetailViewControllerDelegate来使得它遵循这一协议。

    打开AllListsViewController.swift:

    AllListsViewController: UITableViewController,ListDetailViewControllerDelegate {
    

    还是在AllListsViewController.swift中,扩展一下prepare(for:sender:),

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "ShowChecklist" {
                let controller = segue.destination as! ChecklistViewController
                controller.checklist = sender as! Checklist
            } else if segue.identifier == "AddChecklist" {
                let navigationController = segue.destination as! UINavigationController
                let controller = navigationController.topViewController as! ListDetailViewController
                controller.delegate = self
                controller.checklistToEdit = nil
            }
        }
    

    第一个if中的内容不要改动,从else if开始添加新内容。

    这段代码的作用和以前一样,旬斋导航控制器中的视图控制器,并且设置它的delegate为self。

    在AllListsViewController.swift的底部,添加协议方法:

    func listDetailViewControllerDidCancel(_ controller: ListDetailViewController) {
            dismiss(animated: true, completion: nil)
        }
        
        func listDetailViewController(_ controller: ListDetailViewController, didFinishAdding checklist: Checklist) {
            let newRowIndex = lists.count
            lists.append(checklist)
            let indexPath = IndexPath(row: newRowIndex, section: 0)
            let indexPaths = [indexPath]
            tableView.insertRows(at: indexPaths, with: .automatic)
            dismiss(animated: true, completion: nil)
        }
        
        func listDetailViewController(_ controller: ListDetailViewController, didFinishEditing checklist: Checklist) {
            if let index = lists.index(of: checklist) {
                let indexPath = IndexPath(row: index, section: 0)
                if let cell = tableView.cellForRow(at: indexPath) {
                    cell.textLabel!.text = checklist.name
                }
            }
            dismiss(animated: true, completion: nil)
        }
    

    这些方法会在用户点击Cancel或者Done按钮时被调用。

    这些代码你都应该很熟悉才对,我们之前都有完整的做过一次。

    同时添加table view的数据源方法来允许用户删除某一条记录:

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
            lists.remove(at: indexPath.row)
            let indexPaths = [indexPath]
            tableView.deleteRows(at: indexPaths, with: .automatic)
        }
    

    运行app,现在你可以新增或者删除待办事项分类了:

    好玩吧

    ⚠️:如果app崩溃了,那么就检查一下是不是所有的链接都做好了。任何一点细节的丢失,都会导致app崩溃。

    你还无法对已经存在的条目进行修改,然我们来完成这最后一点代码。

    之前我们也是通过转场的方式进入到编辑界面,但是这一次我们不这样做,我们要通过手动的方式来从故事模版中读取这个新的视图控制器,多掌握一些方法总是好的。

    打开AllListsViewController.swift,添加一个tableView(accessoryButtonTappedForRowWith)方法。这个方法是table view的委托方法之一,其作用就和名字一样一目了然。

    override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
            let navigationController = storyboard!.instantiateViewController(withIdentifier: "ListDetailNavgationController") as! UINavigationController
            let controller = navigationController.topViewController as! ListDetailViewController
            controller.delegate = self
            let checklist = lists[indexPath.row]
            controller.checklistToEdit = checklist
            present(navigationController, animated: true, completion: nil)
        }
    

    在这个方法内,你为Add/Edit Checklist界面创建了新的视图控制器对象,并且将其展现在屏幕上。这和转场的作用大致相似。这个视图控制器被嵌入到故事模版中,并且你请求故事模版对象读取它。

    你是在哪里获取这个故事模版对象的呢?每个视图控制器都有一个storyboard属性来引用这个视图控制器是从哪个故事模版中被读取的。你可以使用这个属性来故事模版的所有功能,比如实例化其他视图控制器。

    这个storyboard属性是可选型,因为视图控制器并不全部从故事模版中读取,但是我们眼下的这个是,所以我们使用感叹号对其解包。因为我们可以确定在我们这个app中storyboard不会为nil,所以直接用感叹号强制解包就可以,而不需要用if let的方式。

    调用instantiateViewController(withIdentifier)时用到了一个字符串“ListDetailNavigationController”,这就是请求故事模版创建新视图控制器的方式,在我们这个例子中,这个新的视图控制器就是包含ListDetailViewController的导航控制器。

    你可以直接实例化ListDetailViewController,但是ListDetailViewController是嵌入在导航控制器内部的,如果直接实例化它而不管导航控制器的话,你就无法看到界面标题,以及Done和Cancel按钮。

    打开故事模版,选择指向List Detail View Controller的导航控制器,然后打开身份检查器,将Storyboard ID填写为ListDetailNavigationController:

    运行app,点击某一行上的详细信息按钮试试,如果app崩溃了,重新保存一下故事模版再运行一次。

    练习:设置List Detail View Controller的identifier为ListDetailNavigationController,而不是导航控制器,然后运行app看看会发生什么,试着解释一下为什么会这样,如果你可以解释的话,那么证明你已经掌握了这些内容。

    ⚠️:你还能跟上我的步伐吗?
    如果你对这一切非常茫然并且想要放弃的话,千万要打消这个念头。
    学习新的东西本来就是一个枯燥的过程,编程尤其如此。你可以关掉电脑,去睡一觉,过几天以后再重新打开看看。
    说不定就灵关一闪的明白了起来。

    相关文章

      网友评论

      • 大吉__:var checklist: Checklist!
        过会我再讲为什么这必须是个可选型。

        原文中是
        " I’ll explain why the exclamation mark is necessary in a moment. "
        也许是我学swift不深, 我认为?才是可选型, ! 是强制解包. 所以我理解为这里的Checklist!是必须有值, 而不是可选型.是我自己的误解.
        作者翻译应该是对的.
        type cast 翻译类型扮演觉得有点q, 原文中确实举了演员扮演各种角色, 虽然常见的翻译可能是类型转换.
        续赞~
      • 林水溶:又发现个小错误:
        override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
        let navigationController = storyboard!.instantiateViewController(withIdentifier: "ListDetailNavgationController") as! UINavigationController
        let controller = navigationController.topViewController as! ListDetailViewController
        controller.delegate = self
        let checklist = lists[indexPath.row]
        controller.checklistToEdit = checklist
        present(navigationController, animated: true, completion: nil)
        }
        中的Identifier拼错了: "ListDetailNavigationController", 少了个 i

        Gravityprince:@Gravityprince 知道了,方法里面内部参数名缺失indexPath.添上就对了。
        Gravityprince:let checklist = lists[indexPath.row]报错Use of unresolved identifier 'indexPath'
        Billionfan:@林水溶 谢谢你的细心指正,我会尽快修改这里:+1:
      • 林水溶:如果有人在这句: let controller = segue.destination as! ChecklistViewController
        报错: use of undeclared type "ChecklistViewController" 时, 很可能是在之前写名字时和"ChecklistsViewController"搞错了,一个有s另一个没有.

      本文标题:iOS Apprentice中文版-从0开始学iOS开发-第二十

      本文链接:https://www.haomeiwen.com/subject/aayjqxtx.html