在接下来的两课中,我们将通过创建购物清单应用程序将在本系列中学到的知识付诸实践。 在此过程中,您还将学习许多新概念和模式,例如创建自定义模型类和实现自定义委托模式。 我们有很多基础,所以让我们开始吧。
大纲
我们将要创建的购物清单应用程序具有两个功能:管理商品清单和通过从清单中选择商品来创建购物清单。
我们将使用标签栏控制器构建应用程序,以使在两个视图之间快速而直接地切换。 在本课程中,我们重点介绍第一个功能。 在下一课中,我们将对该功能进行最后润饰,并放大该应用程序的第二个功能购物清单。
即使从用户的角度来看,购物清单应用程序并不复杂,但在其开发过程中仍需要做出一些决策。 我们用于存储项目列表的商店类型是什么? 用户可以添加,编辑和删除项目吗? 这些是我们在接下来的两个课程中要解决的问题。
在本课程中,我还将向您展示如何使用虚拟数据为购物清单应用程序添加种子,从而为新用户提供一些起点。 使用数据播种应用程序通常是一个好主意,以帮助新用户快速入门。
1.创建项目
启动Xcode并基于iOS>应用程序部分中的Single View Application模板创建一个新项目。
将项目命名为购物清单,然后输入组织名称和标识符。 将语言设置为Swift ,将设备设置为iPhone 。 确保未选中底部的复选框。 告诉Xcode将项目保存在何处,然后单击Create 。
2.创建列表视图控制器
如您所料,列表视图控制器将成为UITableViewController
的子类。 通过从“ 文件”菜单中选择“ 新建”>“文件...”来创建一个新类。 从iOS>源部分中选择Cocoa Touch Class 。
将类命名为ListViewController
并使其成为UITableViewController
的子类。 取消选中“ 同时创建XIB文件 ”复选框,并确保将“ 语言”设置为Swift 。 告诉Xcode您要在哪里保存该类,然后单击Create 。
打开Main.storyboard ,选择已经存在的视图控制器,然后将其删除。 拖一个 从对象库中删除UITabBarController
实例,然后删除链接到选项卡栏控制器的两个视图控制器。 从对象库中拖动UITableViewController
,在Identity Inspector中将其类设置为ListViewController
,并从选项卡栏控制器到列表视图控制器创建关系。
选择选项卡栏控制器,打开“ 属性”检查器 ,然后通过选中“初始视图控制器 ”复选框将其设置为情节提要的初始视图控制器 。
列表视图控制器必须是导航控制器的根视图控制器。 选择列表视图控制器,然后从“ 编辑器”菜单中选择“ 嵌入”>“导航控制器 ”。
选择列表视图控制器的表视图,并将“ 属性”检查器中的“ 原型单元”设置为0 。
在模拟器中运行该应用程序,以查看所有设置是否正确。 您应该看到一个空的表格视图,顶部带有导航栏,底部带有标签栏。
3.创建项目模型类
我们将如何处理购物清单应用程序中的商品? 换句话说,我们使用什么类型的对象来存储项目的属性,例如其名称,价格和唯一标识每个项目的字符串?
最明显的选择是将项目的属性存储在字典中。 即使这可以很好地工作,但随着应用程序复杂性的提高,它将严重限制并拖慢我们的速度。
对于购物清单应用程序,我们将创建一个自定义模型类。 设置需要做更多的工作,但是这将使开发变得更加容易。
创建一个新的类Item
,并使其成为NSObject
的子类。 告诉Xcode将类保存在何处,然后单击Create 。
物产
打开Item.swift并声明四个属性:
- 类型为
String
uuid
唯一标识每个项目 - 类型
String
name
-
Float
price
- 类型为
Bool
inShoppingList
,以指示购物清单中是否存在该商品
Item
类必须符合NSCoding
协议,这一点至关重要。 稍后将阐明其原因。 看看我们到目前为止所取得的成就。 注释被省略。
import UIKit
class Item: NSObject {
var uuid: String = NSUUID().UUIDString
var name: String = ""
var price: Float = 0.0
var inShoppingList = false
}
每个属性都需要有一个初始值。 我们将name
设置为一个空字符串,将price
为0.0
,并将inShoppingList
为false
。 要设置uuid
的初始值,我们使用一个我们从未见过的类NSUUID
。 此类可帮助我们创建唯一的字符串或UUID。 我们初始化该类的实例,并通过调用UUIDString()
要求它提供UUID
作为字符串。
通过将以下代码片段添加到ListViewController
类的viewDidLoad()
方法来进行尝试。
let item = Item()
print(item.uuid)
运行该应用程序,并查看Xcode控制台中的输出,以查看生成的UUID是什么样的。 您应该会看到以下内容:
C6B81D40-0528-4D2C-BB58-6EF78D3D3DEF
封存
将自定义对象(例如Item
类的实例)保存到磁盘的一种策略是通过称为存档的过程。 我们将使用NSKeyedArchiver
和NSKeyedUnarchiver
来存档和取消存档Item
类的实例。
类前缀NS
表示两个类都在Foundation框架中定义。 NSKeyedArchiver
类采用一组对象并将它们作为二进制数据存储到磁盘。 这种方法的另一个好处是二进制文件通常比包含相同信息的纯文本文件小。
如果我们要使用NSKeyedArchiver
和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:)
。 init(coder:)
使用了与encodeWithCoder(_:)
中使用的相同的键。 这个非常重要。
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?
的对象AnyObject?
,我们将其转换为String
对象。
您永远不应直接调用init(coder:)
和encodeWithCoder(_:)
。 它们仅由操作系统调用。 通过使Item
类符合NSCoding
协议,我们仅告诉操作系统如何编码和解码该类的实例。
创建实例
为了简化Item
类的新实例的创建,我们创建了一个接受名称和价格的自定义初始化程序。 这是可选的,但是它将使开发更加容易,正如您将在本课程的后面看到的那样。
打开Item.swift并添加以下初始化程序。 请记住,初始化器的名称前没有func
关键字。 我们首先调用超类NSObject
的初始化程序。 然后,我们设置Item
实例的name
和price
属性。
init(name: String, price: Float) {
super.init()
self.name = name
self.price = price
}
你可能会奇怪,为什么我们使用self.name
中init(name:price:)
和name
中init(coder:)
设置name
属性。 在这两种情况下, self
引用我们正在与之交互的Item
实例。 在Swift中,无需使用self
关键字即可访问属性。 但是,在init(name:price:)
中,参数之一的名称与Item
类的属性之一相同。 为避免混淆,我们使用self
关键字。 简而言之,除非引起混淆,否则您可以省略self
关键字来访问属性。
4.加载和保存项目
数据持久性将成为购物清单应用程序中的关键,因此让我们看一下如何实现数据持久性。 打开ListViewController.swift并声明[Item]
类型的变量存储属性items
。 请注意, 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?
类型的可选内容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
}
}
}
我们还没有介绍的是if
语句中的where
关键字。 通过使用where
关键字,我们向if
语句的条件添加了附加约束。 在loadItems()
,我们确保pathForItems()
返回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。 请注意,我们在生成的NSURL
实例上调用path()
以确保我们返回String
对象。
保存项目
即使我们在本课程的稍后部分都不会保存项目,但是在我们使用它的同时实现它也是一个好主意。 感谢pathForItems()
帮助程序方法, saveItems()
的实现非常简洁。
我们首先获取包含应用程序项列表的文件的路径,然后将items
属性的内容写入该位置。 简单。 对?
private func saveItems() {
if let filePath = pathForItems() {
NSKeyedArchiver.archiveRootObject(items, toFile: filePath)
}
}
将对象图写入磁盘的过程称为归档。 我们使用NSKeyedArchiver
类通过调用来完成这个archiveRootObject(_:toFile:)
上NSKeyedArchiver
。
在此过程中, encodeWithCoder(_:)
对象图中的每个对象发送一条encodeWithCoder(_:)
消息,以将其转换为二进制数据。 请记住,几乎不需要直接调用encodeWithCoder(_:)
。
若要验证是否可以从磁盘加载项目列表,请将打印语句添加到ListViewController
类的viewDidLoad()
方法。 在模拟器中运行该应用程序,然后检查是否一切正常。
override func viewDidLoad() {
super.viewDidLoad()
print(items)
}
如果您在Xcode控制台中查看输出,您会注意到items
属性等于空数组。 这就是我们目前所期望的。 重要的是items
不等于nil
。 在下一步中,我们将为用户提供一些要处理的项目,该过程称为seeding 。
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
数组的内容写入磁盘,就像在ListViewController
的saveItems()
方法中看到的saveItems()
。
如果操作成功结束,则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的内容成功为数据存储添加了种子。 请注意,为数据存储播种或更新数据库需要时间。 如果操作花费的时间太长,系统可能会在有机会完成启动之前终止您的应用程序。 苹果将此事件称为看门狗杀死了您的应用程序。
您的应用程序有有限的启动时间。 如果在该时间段内无法启动,则操作系统将终止您的应用程序。 这意味着您必须仔细考虑何时何地执行某些操作,例如为应用程序的数据存储添加种子。
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:添加按钮
向导航栏添加按钮需要一行代码。 重新访问ListViewController
类的viewDidLoad()
方法,并对其进行更新以反映以下实现。
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
以实例UIBarButtonItem
通过调用init(barButtonSystemItem:target:action:)
,在传递.Add
的第一个参数。 第一个参数的类型为UIBarButtonSystemItem
(枚举)。 结果是UIBarButtonItem
的系统提供的实例。
即使我们已经遇到了目标动作模式, init(barButtonSystemItem:target:action:)
的第二个和第三个参数也需要说明。 每当点击导航栏中的按钮时,就会向target
(即self
或ListViewController
实例)发送一条addItem(_:)
消息。
如我所说,将按钮的触摸事件连接到情节提要中的动作时,我们已经遇到了目标动作模式。 这非常相似,唯一的区别是该连接是通过编程方式进行的。
目标行为模式是可可中的常见模式。 这个想法很简单。 对象保留对需要发送的消息和目标的引用,而目标则充当该消息的接收者。
该消息存储为选择器。 等一下。 什么是选择器? 选择器是用于选择期望对象执行的方法的名称或唯一标识符。 您可以在Apple的Cocoa Core能力指南中阅读有关选择器的更多信息。
在模拟器中运行应用程序之前,我们需要在列表视图控制器中创建相应的addItem(_:)
方法。 如果我们不这样做,则在点击按钮并引发异常时,视图控制器将无法响应其收到的消息,从而使应用程序崩溃。
在下一个代码片段中查看方法定义的格式。 如本系列前面所述,该动作接受一个参数,即将消息发送到视图控制器(目标)的对象。 在此示例中,发件人是导航栏中的按钮。
func addItem(sender: UIBarButtonItem) {
print("Button was tapped.")
}
我已经在该方法的实现中添加了一条print语句,以测试是否一切正常。 生成项目并运行应用程序以测试导航栏中的按钮。
步骤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
实例从“ 对象库”拖到工作区 ,并将其类设置为Identity Inspector中的 AddItemViewController
。
通过按Control并将其从“ 列表视图控制器”对象拖到“ 添加项目视图控制器”对象中,来创建手动设置。 从弹出的菜单中选择“ 模态呈现 ”。
选择刚创建的序列 ,打开“ 属性”检查器 ,并将其“ 标识符”设置为AddItemViewController 。
在添加文本字段之前,请选择添加项目视图控制器,然后通过从“ 编辑器”菜单中选择“ 嵌入”>“导航控制器” ,将其嵌入到导航控制器中 。
在本课程的前面,我们以编程方式将UIBarButtonItem
添加到列表视图控制器的导航项。 让我们找出它在情节提要中是如何工作的。 放大“添加项目”视图控制器,并将两个UIBarButtonItem
实例添加到其导航栏中,在每侧放置一个。 选择左栏按钮项,打开“ 属性”检查器 ,然后将“ 标识符”设置为“ 取消” 。 对右栏按钮项目执行相同的操作,将其标识符设置为Save 。
选择“ 添加项目视图控制器”对象,打开右侧的“ 连接检查器 ”,然后将cancel(_:)
操作与左栏按钮项连接,将save(_:)
操作与右栏按钮项连接。
将两个UITextField
实例从“ 对象库”拖动到添加项视图控制器的视图。 如下所示放置文本字段。 不要忘记在文本字段中添加必要的约束。
选择顶部的文本字段,打开“ 属性”检查器 ,然后在“ 占位符”字段中输入“ 名称 ”。 选择底部的文本字段,然后在“ 属性”检查器中将其占位符文本设置为Price ,将键盘设置为Number Pad 。 这样可以确保用户只能在底部的文本字段中输入数字。 选择“ 添加项目视图控制器”对象,打开“ 连接检查器” ,然后将nameTextField
和priceTextField
出口与视图控制器视图中的相应文本字段连接。
那是相当多的工作。 我们在情节提要中所做的一切也可以通过编程来完成。 一些开发人员甚至不使用情节提要,而是以编程方式创建整个应用程序的用户界面。 无论如何,这正是幕后发生的事情。
步骤3:实现addItem(_:)
准备使用AddItemViewController
,让我们重新访问ListViewController
的addItem(_:)
操作。 如下面所示, addItem(_:)
很简短。 我们调用performSegueWithIdentifier(_:sender:)
,传入我们在情节AddItemViewController
设置的AddItemViewController
标识符和视图控制器self
。
func addItem(sender: UIBarButtonItem) {
performSegueWithIdentifier("AddItemViewController", sender: self)
}
步骤4:关闭视图控制器
用户还应该能够通过点击添加项目视图控制器的“取消”或“保存”按钮来关闭视图控制器。 重新访问AddItemViewController
的cancel(_:)
和save(_:)
操作并更新其实现,如下所示。 在本教程的稍后部分,我们将重新讨论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.swift并在顶部声明AddItemViewControllerDelegate
协议。 该协议定义了一种方法,用于通知委托人项目已保存。 它会传递商品的名称和价格。
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?
...
}
正如我在关于表视图的课程中提到的那样,将消息的发送者(每个对象通知委托对象)作为每个委托方法的第一个参数传递是一种很好的做法。 这使得委托对象可以轻松与发送方进行通信,而无需严格要求保留对委托的引用。
通知代表
现在该使用我们刚才声明的委托协议了。 重新访问AddItemViewController
类中的save(_:)
方法,并更新其实现,如下所示。
@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
属性具有值时才调用该方法。 可选链接使您的代码更安全。
还要注意,我们从存储在priceAsString
的值创建了一个Float
。 这是必需的,因为委托方法期望将float作为其第三个参数而不是字符串。
响应保存事件
最后一个难题是使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()
}
我们通过调用init(name:price:)
创建一个新的Item
实例,传入我们从添加项目视图控制器收到的名称和价格。 在下一步中,通过添加新创建的项目来更新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
。
再运行一次该应用程序,以查看一切如何协同工作,就像魔术一样。
结论
可以接受很多东西,但是我们已经完成了很多工作。 在下一课中,我们对列表视图控制器进行一些更改,以编辑和删除列表中的项目。 在该课程中,我们还添加了从商品列表创建购物清单的功能。
如果您有任何问题或意见,可以将其留在下面的评论中,或通过Twitter与我联系。