iOS 从零开始使用 Swift:构建购物清单应用程序 1

iOS 从零开始使用 Swift系列:
探索 iOS SDK
探索 Foundation 框架
使用 UIKit 的第一步
自动布局基础
表格视图基础
导航控制器和视图控制器层次结构
iOS 上的数据持久性和沙盒
构建购物清单应用程序 1
构建购物清单应用程序 2
下一步该去哪里

在接下来的两节课中,我们将通过创建购物清单应用程序将我们在本系列中学到的知识付诸实践。在此过程中,您还将学习许多新概念和模式,例如创建自定义模型类和实现自定义委托模式。我们有很多内容要覆盖,所以让我们开始吧。

大纲

我们将要创建的购物清单应用程序有两个功能,管理物品清单和通过从清单中选择物品来创建购物清单。

我们将使用标签栏控制器构建应用程序,以使两个视图之间的切换快速而直接。在本课中,我们将重点介绍第一个功能。在下一课中,我们将对该功能进行收尾工作并放大购物清单,这是应用程序的第二个功能。

尽管从用户的角度来看购物清单应用程序并不复杂,但在其开发过程中需要做出几个决定。我们使用什么类型的商店来存储商品列表?用户可以添加、编辑和删除项目吗?这些是我们在接下来的两节课中要解决的问题。

在本课中,我还将向您展示如何使用虚拟数据为购物清单应用程序播种,以便为新用户提供一些开始。用数据植入应用程序通常是帮助新用户快速上手的好主意。

1. 创建项目

启动 Xcode 并基于 iOS > Application部分中的Single View Application 模板创建一个新项目。

将项目命名为 Shopping List 并输入组织名称和标识符。将Language 设置为Swift并将 Devices设置为 iPhone。确保底部的复选框未选中。告诉 Xcode 将项目保存在哪里,然后单击 Create

2. 创建列表视图控制器

如您所料,列表视图控制器将是 UITableViewController.  通过从“ 文件 ”菜单中选择“新建”>“文件… ”来创建一个新类 。从 iOS > Source部分中选择 Cocoa Touch Class 。

命名该类 ListViewController 并使其成为 UITableViewController. 保留复选框Also create XIB file未选中并确保Languages 设置为 Swift。告诉 Xcode 您要将类保存在哪里,然后单击 Create

打开Main.storyboard,选择已经存在的视图控制器,然后将其删除。拖动一个 UITabBarController 从 对象库中删除实例 并删除链接到选项卡栏控制器的两个视图控制器。UITableViewController从 Object Library中拖动 a ,ListViewController 在 Identity Inspector中将其类设置为 ,并创建从标签栏控制器到列表视图控制器的关系segue。

选择标签栏控制器,打开 Attributes Inspector ,并通过选中Is Initial View Controller复选框使其成为情节提要的初始视图 控制器。

列表视图控制器需要是导航控制器的根视图控制器。选择列表视图控制器并  从 编辑器 菜单中选择嵌入 > 导航控制器。

选择列表视图控制器的表格视图并将 属性检查器 中的 原型单元设置 为0

在模拟器中运行应用程序,看看是否一切都设置正确。您应该会看到一个空的表格视图,顶部有一个导航栏,底部有一个标签栏。

3. 创建项目模型类

我们将如何处理购物清单应用程序中的项目?换句话说,我们使用什么类型的对象来存储一个项目的属性,例如它的名称、价格和唯一标识每个项目的字符串?

最明显的选择是将项目的属性存储在字典中。尽管这可以正常工作,但随着应用程序的复杂性增加,它会严重限制并减慢我们的速度。

对于购物清单应用程序,我们将创建一个自定义模型类。它需要更多的工作来设置,但它会使开发变得更加容易。

创建一个新类 , Item并使其成为 的子类 NSObject。告诉 Xcode 将类保存在哪里,然后单击 Create

特性

打开Item.swift并声明四个属性:

  • uuid of type String to uniquely identify each item
  • name of type String
  • price of type Float
  • inShoppingList of type Bool to indicate if the item is present in the shopping list
  • uuidString 唯一标识每个项目 的类型 
  • name 类型 String
  • price 类型 Float
  • inShoppingList 类型 Bool 以指示该项目是否存在于购物清单中

该类必须 Item 符合 NSCoding 协议。稍后将清楚其原因。看看我们到目前为止得到了什么。注释被省略。

import UIKit
 
class Item: NSObject {
 
    var uuid: String = NSUUID().UUIDString
    var name: String = ""
    var price: Float = 0.0
    var inShoppingList = false
 
}

每个属性都需要有一个初始值。我们将to和to设置name为空字符串。为了设置 的初始值,我们使用了一个我们以前没见过的类,。这个类帮助我们创建一个唯一的字符串或 UUID。我们初始化该类的一个实例,并通过调用来请求它作为字符串。price0.0inShoppingListfalseuuidNSUUIDUUIDUUIDString()

通过将以下代码片段添加到类的viewDidLoad()方法来尝试一下ListViewController

let item = Item()
print(item.uuid)

运行应用程序并查看 Xcode 控制台中的输出以查看生成的 UUID 的样子。您应该看到如下内容:

C6B81D40-0528-4D2C-BB58-6EF78D3D3DEF

归档

将自定义对象(例如 Item 类的实例)保存到磁盘的一种策略是通过称为归档的过程。我们将使用 NSKeyedArchiver and NSKeyedUnarchiver 来归档和取消归档 Item 类的实例。

类前缀NS表示这两个类都是在 Foundation 框架中定义的。该类 NSKeyedArchiver 采用一组对象并将它们作为二进制数据存储到磁盘。这种方法的另一个好处是二进制文件通常比包含相同信息的纯文本文件小。

如果我们要使用 NSKeyedArchiver and NSKeyedUnarchiver 来归档和取消归档 Item 类的实例,Item需要采用 NSCoding 协议。让我们从更新Item类开始告诉编译器Item采用NSCoding协议。

import UIKit
 
class Item: NSObject, NSCoding {
 
    ...
 
}

请记住,在有关 Foundation 框架的课程中, NSCoding 协议声明了类必须实现的两个方法,以允许对类的实例进行编码和解码。让我们看看这是如何工作的。

编码

如果您创建自定义类,则您负责指定该类的实例应如何编码、转换为二进制数据。在 encodeWithCoder(_:)中,符合 NSCoding 协议的类指定了类的实例应该如何编码。看看下面的实现。我们使用的键并不那么重要,但为了清楚起见,您通常希望使用属性名称。

func encodeWithCoder(coder: NSCoder) {
    coder.encodeObject(uuid, forKey: "uuid")
    coder.encodeObject(name, forKey: "name")
    coder.encodeFloat(price, forKey: "price")
    coder.encodeBool(inShoppingList, forKey: "inShoppingList")
}

解码

每当需要将编码对象转换回相应类的实例时,init(coder:)都会调用它。encodeWithCoder(_:) 我们在中使用 的相同键 用于init(coder:). 这个非常重要。

required init?(coder decoder: NSCoder) {
    super.init()
     
    if let archivedUuid = decoder.decodeObjectForKey("uuid") as? String {
        uuid = archivedUuid
    }
     
    if let archivedName = decoder.decodeObjectForKey("name") as? String {
        name = archivedName
    }
     
    price = decoder.decodeFloatForKey("price")
    inShoppingList = decoder.decodeBoolForKey("inShoppingList")
}

请注意,我们使用required关键字。请记住,我们required在本系列的前面介绍了关键字。因为 decodeObjectForKey(_:)返回一个类型的对象AnyObject?,所以我们将它转​​换为一个String对象。

你永远不应该直接调用init(coder:)and  encodeWithCoder(_:)。它们仅由操作系统调用。通过使 Item 类 符合NSCoding 协议,我们只告诉操作系统如何编码和解码类的实例。

创建实例

为了使创建 Item 类的新实例更容易,我们创建了一个接受名称和价格的自定义初始化程序。这是可选的,但它将使开发更容易,正如您将在本课后面看到的那样。

打开 Item.swift 并添加以下初始化程序。func请记住,初始化程序的名称前面没有关键字。我们首先调用超类的初始化器,NSObject. 然后我们设置实例的nameprice属性。Item

init(name: String, price: Float) {
    super.init()
     
    self.name = name
    self.price = price
}

您可能想知道为什么我们使用self.nameininit(name:price:)nameininit(coder:)来设置name属性。在这两种情况下,都self引用Item我们正在与之交互的实例。在 Swift 中,您可以在不使用self关键字的情况下访问属性。但是,init(name:price:)其中一个参数的名称与类的属性之一相同Item。为避免混淆,我们使用self关键字。简而言之,您可以省略self关键字来访问属性,除非有引起混淆的原因。广告

4. 加载和保存项目

数据持久性将成为我们的购物清单应用程序的关键,所以让我们看看我们将如何实现数据持久性。打开ListViewController.swift 并声明一个items类型为 的变量存储属性[Item]。请注意, 的初始值items是一个空数组。

import UIKit
 
class ListViewController: UITableViewController {
 
    var items = [Item]()
 
    ...
 
}

视图控制器的表格视图中显示的项目将存储在 items. 重要的是它 items 是一个可变数组,因此是var关键字。为什么?我们将在本课的稍后部分添加添加新项目的功能。

在类的初始化程序中,我们从磁盘加载项目列表并将其存储在 items 我们刚才声明的属性中。

// MARK: -
// MARK: Initialization
required init?(coder decoder: NSCoder) {
    super.init(coder: decoder)
     
    // Load Items
    loadItems()
}

视图控制器的 loadItems() 方法只不过是一个帮助方法,以保持init?(coder:) 方法的简洁和可读性。我们来看看 loadItems().

装载物品

该 loadItems() 方法首先获取存储项目列表的文件的路径。我们通过调用 pathForItems()另一个辅助方法来做到这一点,稍后我们将介绍它。因为pathForItems()返回一个可选的类型String?,我们将结果绑定到一个常量,filePath。我们在本系列的前面讨论了可选绑定。

// MARK: -
// MARK: Helper Methods
private func loadItems() {
    if let filePath = pathForItems() where NSFileManager.defaultManager().fileExistsAtPath(filePath) {
        if let archivedItems = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [Item] {
            items = archivedItems
        }
    }
}

我们尚未涵盖的是语句中的where关键字。if通过使用关键字,我们为语句where的条件添加了一个额外的约束。ifloadItems()中,我们确保pathForItems()返回 a String。使用where子句,我们还验证 的值是否filePath对应于磁盘上的文件。我们为此使用NSFileManager该类。

NSFileManager 是一个我们还没有使用过的类。它提供了一个易于使用的 API 来处理文件系统。我们通过询问默认管理器来获得对类实例的引用。

然后我们调用fileExistsAtPath(_:) 默认管理器,传入我们在第一行获得的文件路径loadItems()。如果文件路径指定的位置存在文件,我们将文件的内容加载到 items 属性中。如果该位置不存在文件,则该items属性将保留其初始值,即一个空数组。

加载文件的内容是通过 NSKeyedUnarchiver 类完成的。Item 它可以读取文件中包含的二进制数据并将其转换为对象图、实例数组 。当我们 saveItems() 在一分钟内查看该方法时,这个过程将变得更加清晰。

现在让我们看一下pathForItems()我们之前调用的辅助方法。 我们首先获取应用程序沙箱中Documents目录的路径 。这一步现在应该很熟悉了。

private func pathForItems() -> String? {
    let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
     
    if let documents = paths.first, let documentsURL = NSURL(string: documents) {
        return documentsURL.URLByAppendingPathComponent("items.plist").path
    }
     
    return nil
}

该方法返回包含应用程序项目列表的文件的路径。我们通过获取应用程序沙箱的Documents目录的路径并附"items"加到它来做到这一点。

使用的美妙之处 URLByAppendingPathComponent(_:) 在于,在任何需要的地方都可以为我们插入路径分隔符。换句话说,系统确保我们收到一个有效的文件 URL。请注意,我们path()在结果NSURL实例上调用以确保我们返回一个String对象。

保存项目

尽管我们要等到本课的后面才会保存项目,但在我们使用它的同时实现它是一个好主意。saveItems() 得益于 pathForItems() 辅助方法,实现 非常简洁。

我们首先获取包含应用程序项目列表的文件的路径,然后将 items 属性的内容写入该位置。简单的。正确的?

private func saveItems() {
    if let filePath = pathForItems() {
        NSKeyedArchiver.archiveRootObject(items, toFile: filePath)
    }
}

将对象图写入磁盘的过程称为归档。我们使用 NSKeyedArchiver 该类通过调用来完成此 archiveRootObject(_:toFile:) 操作NSKeyedArchiver

在这个过程中,对象图中的每个对象都被发送一个消息, encodeWithCoder(_:) 将其转换为二进制数据。请记住,很少需要直接调用 encodeWithCoder(_:).

要验证从磁盘加载项目列表是否有效,请将 print 语句添加到类的 viewDidLoad() 方法中 ListViewController 。在模拟器中运行应用程序并检查一切是否正常。

override func viewDidLoad() {
    super.viewDidLoad()
     
    print(items)
}

如果您查看 Xcode 控制台中的输出,您会注意到该 items 属性等于 en 空数组。这就是我们目前所期望的。重要的 items 是 不等于 nil。在下一步中,我们将为用户提供一些可以使用的项目,这个过程称为 播种

5. 播种数据存储

使用数据植入应用程序通常意味着参与用户和使用应用程序不到一分钟后退出应用程序的用户之间的差异。使用虚拟数据植入应用程序不仅可以帮助用户加快速度,还可以向新用户展示应用程序在其中包含数据时的外观和感觉。

在购物清单应用程序中植入初始物品清单并不困难。因为我们不想创建重复项,所以我们在应用程序启动期间检查数据存储是否已经用数据播种。如果数据存储尚未播种,我们会加载一个包含种子数据的列表,并使用该列表创建应用程序的数据存储。

可以从应用程序中的多个位置调用用于播种数据存储的逻辑,但提前考虑很重要。我们可以将用于播种数据存储的逻辑放在 ListViewController 类中,但是如果在未来版本的应用程序中,其他视图控制器也可以访问项目列表怎么办。为数据存储播种的更好位置是在 AppDelegate 课堂上。让我们看看这是如何工作的。

打开 AppDelegate.swift 并修改 的实现, application(_:didFinishLaunchingWithOptions:) 如下所示。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Seed Items
    seedItems()
     
    return true
}

与之前实现的唯一区别是我们首先调用 seedItems(). 在任何视图控制器初始化之前为数据存储播种是很重要的,因为数据存储需要在任何视图控制器加载项目列表之前播种。

实现 seedItems() 并不复杂。我们首先存储对共享用户默认对象的引用,然后我们检查用户默认数据库是否有一个带有名称的键的条目, "UserDefaultsSeedItems" 以及这个条目是否是一个值为 的布尔值true

// MARK: -
// MARK: Helper Methods
private func seedItems() {
    let ud = NSUserDefaults.standardUserDefaults()
     
    if !ud.boolForKey("UserDefaultsSeedItems") {
        if let filePath = NSBundle.mainBundle().pathForResource("seed", ofType: "plist"), let seedItems = NSArray(contentsOfFile: filePath) {
            // Items
            var items = [Item]()
             
            // Create List of Items
            for seedItem in seedItems {
                if let name = seedItem["name"] as? String, let price = seedItem["price"] as? Float {
                    // Create Item
                    let item = Item(name: name, price: price)
                     
                    // Add Item
                    items.append(item)
                }
            }
             
            if let itemsPath = pathForItems() {
                // Write to File
                if NSKeyedArchiver.archiveRootObject(items, toFile: itemsPath) {
                    ud.setBool(true, forKey: "UserDefaultsSeedItems")
                }
            }
        }
    }
}

只要您在命名您使用的键时保持一致,键就可以是您喜欢的任何键。用户默认数据库中的键告诉我们应用程序是否已经植入数据。这很重要,因为我们只想为应用程序播种一次。

如果应用程序还没有播种,我们从应用程序包中加载一个属性列表,  seed.plist。该文件包含一个字典数组,每个字典代表一个带有名称和价格的项目。

在遍历 seedItems 数组之前,我们创建一个可变数组来存储 Item 我们将要创建的实例。对于数组中的每个字典 ,我们 通过调用我们在本课前面声明的初始化程序来seedItems 创建一个 实例。Item每个项目都添加到items 数组中。

最后,我们创建将存储项目列表的文件的路径,并将 items 数组的内容写入磁盘,就像我们 saveItems() 在 ListViewController.

如果操作成功结束,则该方法 archiveRootObject(_:toFile:) 返回 true ,然后我们通过将键的布尔值设置为来更新用户默认 "UserDefaultsSeedItems" 数据库 true。下次启动应用程序时,数据存储将不再被播种。

您可能已经注意到我们在seedItems(), 中使用了另一个辅助方法pathForItems()。它的实现与 ListViewController 类的实现相同。

private func pathForItems() -> String? {
    let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
     
    if let documents = paths.first, let documentsURL = NSURL(string: documents) {
        return documentsURL.URLByAppendingPathComponent("items").path
    }
     
    return nil
}

在运行应用程序之前,请确保将属性列表 seed.plist复制到您的项目中。只要它包含在应用程序的捆绑包中,存储它的位置并不重要。

运行应用程序并检查控制台中的输出,以查看数据存储是否已成功使用 seed.plist的内容播种。请注意,为数据存储植入数据或更新数据库需要时间。如果操作时间过长,系统可能会在您的应用程序有机会完成启动之前将其终止。Apple 将此事件称为监视 程序杀死您的应用程序。

您的应用程序的启动时间有限。如果它未能在该时间范围内启动,则操作系统会终止您的应用程序。这意味着您必须仔细考虑何时何地执行某些操作,例如为应用程序的数据存储播种。

6. 显示项目列表

我们现在有一个可以使用的项目列表。在列表视图控制器的表格视图中显示项目并不困难。看一下 UITableViewDataSource 下面显示的协议的三种方法的实现。如果您已阅读有关表视图的教程,那么这些实现应该看起来很熟悉。

// MARK: -
// MARK: Table View Data Source Methods
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}
 
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return items.count
}
 
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // Dequeue Reusable Cell
    let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath)
     
    // Fetch Item
    let item = items[indexPath.row]
     
    // Configure Table View Cell
    cell.textLabel?.text = item.name
 
    return cell
}

在运行应用程序之前,我们需要注意两个细节,声明常量 CellIdentifier并告诉表格视图使用哪个类来创建表格视图单元格。

import UIKit
 
class ListViewController: UITableViewController {
 
    let CellIdentifier = "Cell Identifier"
 
    ...
 
}

当您使用它时,将title列表视图控制器的属性设置为"Items".

override func viewDidLoad() {
    super.viewDidLoad()
     
    title = "Items"
     
    // Register Class
    tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
}

在模拟器中运行应用程序。这是您应该在模拟器中看到的内容。

7. 添加项目 – 第 1 部分

无论我们如何精心制作种子项目列表,用户肯定会希望将其他项目添加到列表中。在 iOS 上,将新项目添加到列表中的一种常见方法是向用户呈现一个模态视图控制器,在该控制器中可以输入新数据。这意味着我们需要:

  • 向用户界面添加一个按钮以添加新项目
  • 创建一个视图控制器来管理接受用户输入的视图
  • 根据用户的输入创建一个新项目
  • 将新创建的项目添加到表视图

第 1 步:添加按钮

向导航栏添加按钮需要一行代码。重新访问类的 viewDidLoad() 方法 ListViewController 并更新它以反映下面的实现。

override func viewDidLoad() {
    super.viewDidLoad()
     
    // Register Class
    tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
     
    // Create Add Button
    navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "addItem:")
}

在关于标签栏控制器的课程中,您了解到每个视图控制器都有一个 tabBarItem 属性。类似地,每个视图控制器都有一个 属性,一个 在父视图控制器的导航栏中表示视图控制器navigationItem 的唯一实例 ——导航控制器。UINavigationItem

该 navigationItem 属性有一个 leftBarButtonItem 属性,即 的实例 UIBarButtonItem,它引用显示在导航栏左侧的栏按钮项。该 navigationItem 属性也有一个 titleView 和一个 rightBarButtonItem 属性。

viewDidLoad()中,我们  通过调用 leftBarButtonItem 将视图控制器的属性 设置navigationItem 为 的实例 ,并 作为第一个参数传入 。第一个参数是类型,一个枚举。结果是系统提供的 .UIBarButtonIteminit(barButtonSystemItem:target:action:).AddUIBarButtonSystemItemUIBarButtonItem

尽管我们已经遇到了目标-动作模式,但 init(barButtonSystemItem:target:action:) 需要解释一下第二个和第三个参数。每当点击导航栏中的按钮时, addItem(_:) 都会向 target,即, self 或 ListViewController 实例发送一条消息。

正如我所说,当我们将按钮的触摸事件连接到情节提要中的动作时,我们已经遇到了目标动作模式。这非常相似,唯一的区别是连接是以编程方式进行的。

目标-动作模式是 Cocoa 中的一种常见模式。这个想法很简单。对象保留对需要发送的消息和目标的引用,目标是充当该消息的接收者的对象。

消息存储为选择器。等一下。什么是选择器?选择器是用于选择对象预期执行的方法的名称或唯一标识符。您可以在 Apple 的Cocoa Core Competencies 指南中阅读有关选择器的更多信息 。

在模拟器中运行应用程序之前,我们需要 addItem(_:) 在列表视图控制器中创建相应的方法。如果我们不这样做,当点击按钮并抛出异常时,视图控制器将无法响应它收到的消息,从而导致应用程序崩溃。

在下一个代码片段中查看方法定义的格式。正如我们在本系列前面所见,动作接受一个参数,即发送消息到视图控制器(目标)的对象。在此示例中,发送者是导航栏中的按钮。

func addItem(sender: UIBarButtonItem) {
    print("Button was tapped.")
}

我在方法的实现中添加了一个打印语句来测试一切是否正常。构建项目并运行应用程序以测试导航栏中的按钮。

第 2 步:创建视图控制器

创建一个新的 UIViewController 子类并将其命名为 AddItemViewController。在AddItemViewController.swift中,我们为两个文本字段声明了两个出口,稍后我们将创建它们。

import UIKit
 
class AddItemViewController: UIViewController {
 
    @IBOutlet var nameTextField: UITextField!
    @IBOutlet var priceTextField: UITextField!
     
    // MARK: -
    // MARK: View Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }
 
}

我们还需要在 AddItemViewController.swift中声明两个动作。第一个操作 cancel(_:)取消了新项目的创建。第二个动作 , save(_:)使用用户的输入来创建和保存一个新项目。

// MARK: -
// MARK: Actions
@IBAction func cancel(sender: UIBarButtonItem) {
     
}
 
@IBAction func save(sender: UIBarButtonItem) {
     
}

打开 Main.storyboard,将一个 UIViewController 实例从 Object Library 拖到工作区AddItemViewController ,并在 Identity Inspector中将其类设置为 。

通过按Control 并从 List View Controller 对象拖动到 Add Item View Controller 对象来创建手动 segue  。 从弹出的菜单中选择 Present Modally 。

选择您刚刚创建的 segue,打开 Attributes Inspector并将其 Identifier设置 为 AddItemViewController

在我们添加文本字段之前,选择添加项目视图控制器并将其 嵌入到导航控制器中,方法 是从 编辑器 菜单中选择嵌入 > 导航控制器。

在本课的前面,我们以编程 UIBarButtonItem 方式向列表视图控制器的导航项添加了一个。让我们看看它是如何在故事板中工作的。放大添加项目视图控制器并将两个 UIBarButtonItem 实例添加到其导航栏,在每一侧放置一个。选择左侧栏按钮项,打开 Attributes Inspector,并将 Identifier设置 为 Cancel。对右栏按钮项执行相同操作,将其Identifier设置 为 Save

选择 Add Item View Controller 对象,打开  右侧 的Connections Inspectorcancel(_:) ,将 action 与左栏按钮项连接起来,将 save(_:) action 与右栏按钮项连接起来。

将两个 UITextField 实例从 对象库拖到 添加项视图控制器的视图中。如下所示放置文本字段。不要忘记向文本字段添加必要的约束。

选择顶部的文本字段,打开 Attributes Inspector ,然后 在 Placeholder 字段中输入 Name 。选择底部的文本字段,然后在 Attributes Inspector中,将其占位符文本设置为 Price 并将 Keyboard 设置为 Number Pad。这确保用户只能在底部文本字段中输入数字。选择 Add Item View Controller 对象,打开 Connections Inspector,然后将  和  出口与视图控制器视图中的相应文本字段连接。nameTextFieldpriceTextField

那是相当多的工作。我们在故事板中所做的一切也可以通过编程方式完成。一些开发人员甚至不使用情节提要并以编程方式创建整个应用程序的用户界面。无论如何,这正是幕后发生的事情。

第 3 步:实施 addItem(_:)

AddItemViewController 准备好使用了,让 我们 addItem(_:) 重温 ListViewController. 的实现addItem(_:) 很短,如下所示。我们调用 performSegueWithIdentifier(_:sender:),传入 AddItemViewController 我们在故事板中设置的标识符和 self视图控制器。

func addItem(sender: UIBarButtonItem) {
    performSegueWithIdentifier("AddItemViewController", sender: self)
}

第 4 步:关闭视图控制器

用户还应该能够通过点击添加项目视图控制器的取消或保存按钮来关闭视图控制器。重新访问 cancel(_:) 和 save(_:) 中的操作 AddItemViewController 并更新它们的实现,如下所示。我们将 save(_:) 在本教程的稍后部分重新讨论该操作。

@IBAction func cancel(sender: UIBarButtonItem) {
    dismissViewControllerAnimated(true, completion: nil)
}
 
@IBAction func save(sender: UIBarButtonItem) {
    dismissViewControllerAnimated(true, completion: nil)
}

当我们调用 dismissViewControllerAnimated(_:completion:) 以模态方式呈现视图的视图控制器时,模态视图控制器将该消息转发给呈现视图控制器的视图控制器。在我们的示例中,这意味着添加项目视图控制器将消息转发到导航控制器,导航控制器又将其转发到列表视图控制器。的第二个参数dismissViewControllerAnimated(_:completion:) 是动画完成时执行的闭包。

在模拟器中运行应用程序以查看 AddItemViewController 该类的运行情况。当您点击名称或价格文本字段时,键盘应自动从底部弹出。

7. 添加项目 – 第 2 部分

列表视图控制器如何知道添加项目视图控制器何时添加了新项目?我们应该保留对显示添加项目视图控制器的列表视图控制器的引用吗?这会引入紧密耦合,这不是一个好主意,因为它使我们的代码不那么独立并且不那么可重用。

我们面临的问题可以通过实现自定义委托协议来解决。让我们看看这是如何工作的。

代表团

这个想法很简单。每当用户点击保存按钮时,添加项目视图控制器会从文本字段中收集信息并通知其委托新项目已保存。

委托对象应该是符合我们定义的自定义委托协议的对象。由委托对象决定需要对添加项视图控制器发送的信息执行什么操作。添加项视图控制器负责捕获用户的输入并通知其委托。

打开 AddItemViewController.swiftAddItemViewControllerDelegate并 在顶部 声明 协议。该协议定义了一种方法,用于通知委托项目已保存。它传递物品的名称和价格。

import UIKit
 
protocol AddItemViewControllerDelegate {
    func controller(controller: AddItemViewController, didSaveItemWithName name: String, andPrice price: Float)
}
 
class AddItemViewController: UIViewController {
 
    ...
 
}

提醒一下,协议声明定义或声明符合协议的对象应实现的方法和属性。Swift 协议中的每个方法和属性都是必需的。

我们还需要为委托声明一个属性。委托的类型是AddItemViewControllerDelegate?。注意问号,表示它是可选类型。

class AddItemViewController: UIViewController {
 
    @IBOutlet var nameTextField: UITextField!
    @IBOutlet var priceTextField: UITextField!
     
    var delegate: AddItemViewControllerDelegate?
     
    ...
 
}

正如我在 关于表视图的课程中提到的,将消息的发送者(通知委托对象的对象)作为每个委托方法的第一个参数传递是一种很好的做法。这使得委托对象可以很容易地与发送者进行通信,而无需严格要求保留对委托的引用。

通知代表

是时候使用我们刚才声明的委托协议了。重新访问 类save(_:) 中的方法 AddItemViewController 并更新其实现,如下所示。

@IBAction func save(sender: UIBarButtonItem) {
    if let name = nameTextField.text, let priceAsString = priceTextField.text, let price = Float(priceAsString) {
        // Notify Delegate
        delegate?.controller(self, didSaveItemWithName: name, andPrice: price)
         
        // Dismiss View Controller
        dismissViewControllerAnimated(true, completion: nil)
    }
}

我们使用可选绑定来安全地提取名称和价格文本字段的值。我们通过调用之前声明的委托方法来通知委托。最后,我们关闭视图控制器。

有两个细节值得指出。你发现代表后面的问号了property吗?在 Swift 中,这种结构称为可选链。因为该delegate属性是可选的,所以不能保证它有一个值。delegate通过在调用委托方法时将问号附加到属性,仅当delegate属性具有值时才调用该方法。可选链接使您的代码更安全。

另请注意,我们Float 从存储在 priceAsString. 这是必要的,因为委托方法需要一个浮点数作为其第三个参数,而不是字符串。

响应保存事件

难题的最后一块是使ListViewController 符合 AddItemViewControllerDelegate 协议。打开 ListViewController.swift 并更新接口声明, ListViewController 使类符合新协议。

import UIKit
 
class ListViewController: UITableViewController, AddItemViewControllerDelegate {
 
    ...
 
}

我们现在需要实现 AddItemViewControllerDelegate 协议中定义的方法。在 ListViewController.swift中,添加以下 controller(_:didSaveItemWithName:andPrice:).

// MARK: -
// MARK: Add Item View Controller Delegate Methods
func controller(controller: AddItemViewController, didSaveItemWithName name: String, andPrice price: Float) {
    // Create Item
    let item = Item(name: name, price: price)
     
    // Add Item to Items
    items.append(item)
     
    // Add Row to Table View
    tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: (items.count - 1), inSection: 0)], withRowAnimation: .None)
     
    // Save Items
    saveItems()
}

Item我们通过调用创建一个新实例init(name:price:),传入我们从添加项目视图控制器收到的名称和价格。在下一步中, items 通过添加新创建的项目来更新属性。当然,表格视图不会自动反映新项目的添加。我们手动将新行插入到表视图中。为了将更改保存到磁盘,我们调用 saveItems() 了我们在本教程前面实现的视图控制器。

设置委托

这个有点复杂的难题的最后一部分是在将添加项目视图控制器呈现给用户时设置它的委托。prepareForSegue(_:sender:) 正如我们在本系列前面看到的那样,我们这样做了 。

// MARK: -
// MARK: Navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "AddItemViewController" {
        if let navigationController = segue.destinationViewController as? UINavigationController,
           let addItemViewController = navigationController.viewControllers.first as? AddItemViewController {
            addItemViewController.delegate = self
        }
    }
}

如果 segue 的标识符等于 AddItemViewController,我们向 segue 询问它的 destinationViewController。您可能认为目标视图控制器是添加项目视图控制器,但请记住,添加项目视图控制器嵌入在导航控制器中。

这意味着我们需要获取导航控制器导航堆栈中的第一项,这为我们提供了根视图控制器或我们正在寻找的添加项目视图控制器对象。然后我们 delegate 将添加项视图控制器的属性 设置self为列表视图控制器。

再运行一次应用程序,看看一切是如何协同工作的——就像变魔术一样。

结论

这需要考虑很多,但我们已经完成了很多。在下一课中,我们对列表视图控制器进行了一些更改,以编辑和删除列表中的项目。在该课程中,我们还添加了从商品列表创建购物清单的功能。

ios-from-scratch-with-swift-building-a-shopping-list-application-1–cms-25515