本教程包含内容:
- Model-View-Controller 工作原理
- 大标题(large titles)展示
- Segue 类型介绍
- 代理(delegate)模式讲解
- 可选类型 Optionals 讲解
- Weak 弱引用讲解
- 沙盒机制讲解
- Codable 协议
- Plist files 序列化讲解
- UserDefaults 讲解
- Functional Programming 讲解
- 本地通知 (local notifications) 讲解
- 类方法 vs 实例方法讲解
- 本教程 demo 下载地址
本 demo 最终效果:
checklists app 流程:
最终 storyboard 是这样的:
Navigation controlers
navigation controller 可能是 iOS 第二常用的 UI 组件,另一个更常用的组件是table view:
Model-View-Controller 工作原理
大标题(large titles)展示
我们可以对导航栏标题(大标题)进行设置。大标题默认情况下未启用,但我们可以通过在 storyboard 中简单设置或写一行代码轻松地启用大标题。
比如在 super.viewDidLoad() 下面一行写下如下代码:
navigationController?.navigationBar.prefersLargeTitles = true
如果我们对大标题效果不满意,可以随时关闭大标题,但注意,当 tableview 有大量的 item 时,需要滚动以查看更多信息,大标题将缩回顶部导航栏,并提供“经典”外观的导航栏。 因此,在决定禁用它之前,不妨尝试一下。
注:Apple 建议不要在所有屏幕上都使用大标题。他们建议,在主屏幕(main screen)以及可能需要醒目标题的时候使用大标题。
Add 按钮
@IBAction func addItem() {
let newRowIndex = items.count
let item = ChecklistItem()
item.text = "I am a new row"
items.append(item)
let indexPath = IndexPath(row: newRowIndex, section: 0)
let indexPaths = [indexPath]
tableView.insertRows(at: indexPaths, with: .automatic)
}
table views 使用 index-paths 标识行。 因此,我们首先用 newRowIndex 变量中的行号创建一个指向新行的 IndexPath 对象。 现在,该索引路径对象指向第 5 行(section 0 中)。
简单回顾一下:
- 创建一个新的 ChecklistItem 对象。
- 将其添加到数据模型。
- 在表格视图中为其插入新行。
滑动删除行
override func tableView(
_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath
) {
// 1
items.remove(at: indexPath.row)
// 2
let indexPaths = [indexPath]
tableView.deleteRows(at: indexPaths, with: .automatic)
}
当我们调用 items.remove(at:) 方法时,这不仅将 ChecklistItem 从数组中取出,而且将其永久销毁。
只要 ChecklistItem 对象位于数组内,该数组就可以对其进行引用。当我们从数组中取出 ChecklistItem 时,引用将消失并且对象将被销毁。
销毁对象意味着什么?
每个对象都占据计算机内存的一小部分。创建对象实例时,计算机将用一块内存来保存对象的数据。如果对象被释放,则该内存将再次变得可用,并最终将被新对象占用。
删除对象后,该对象不再存在于内存中,我们将无法再使用它。
在旧版本的 iOS 上,我们必须手动处理内存管理。幸运的是,现在 Swift 使用一种称为自动引用计数(ARC)的机制来管理应用程序中对象的生存期,从而我们不必再担心它。
Segue 类型
segue 类型简要说明:
** Show**:将新的视图控制器推到导航堆栈上,以便新的视图控制器位于导航堆栈的顶部。它还提供了一个后退按钮以返回到先前的视图控制器。如果视图控制器未嵌入在导航控制器中,则将以模态视图方式显示新的视图控制器。
示例:在 “Mail app” 中浏览文件夹。
Show Detail:用于拆分视图控制器。当处于扩展的两列界面中时,新的视图控制器将替换拆分视图的详细信息视图控制器。否则,如果在单列模式下,它将推入导航控制器。
示例:在“Messages app”中,点击对话将显示对话详细信息-在两列布局中替换右侧的视图控制器,或在单列布局中推送会话。
Present Modally:提供新的视图控制器以覆盖以前的视图控制器-最常用于呈现可覆盖 iPhone 或 iPad 上整个屏幕的视图控制器的情况,通常将其显示为居中的框,以使屏幕变暗展示视图控制器。通常,如果 app 顶部有一个导航栏或在底部有一个标签栏,那么模态视图控制器也将覆盖这些导航栏。
示例:在“设置 app” 中选择 Touch ID 和 密码。
Present as Popover:在 iPad 上运行时,新的视图控制器将出现在Popover中,轻按此 Popover 之外的任何位置将其关闭。在 iPhone上,将以全屏模式呈现新的视图控制器。
示例:点击“日历 app” 中的+按钮
** Custom**:允许我们自定义 segue 并控制其行为。
view controllers 容器
Table View Controller 位于 Navigation Controller 内部。
Navigation Controller 是一种特殊类型的视图控制器,它充当其它视图控制器的容器。它带有导航栏,通过将它们滑动到视线之外,可以轻松地从一个屏幕转到另一个屏幕。
Navigation Controller 只是包含执行实际工作的视图控制器的框架,这些视图控制器被称为“内容”控制器。
比如,这里 ChecklistViewController 提供了第一屏内容。 第二屏的内容来自AddItemViewController。
Static Cells
当我们事先知道表格视图将包含多少节和行时,便可以使用静态单元格(static cells)。
使用静态单元格,可以直接在 storyboard 中设计 rows。 对于具有静态单元格的 table,我们无需提供数据源,并且可以在 outlets 中设置标签等各种属性。
禁止 cell 可选
// MARK: - Table View Delegates
override func tableView(
_ tableView: UITableView,
willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
return nil
}
Table view cells 具有选择颜色属性。即使我们禁止行可编辑,有时 UIKit 仍然会在我们点击它时短暂地将其绘制为灰色。 因此,最好也禁用此选择颜色。
➤在 storyboard 中,选择 Table view cells ,然后转到“属性”检查器,将选择属性设置为无。
现在,运行该应用程序,则无法选择该行并将其变为灰色。
Return 方法
IndexPath 后面的问号是什么?
它告诉 Swift 编译器,此方法允许返回 nil。注意,仅当返回类型后面有问号(或感叹号)时,才允许从方法返回 nil。 后面带有问号的类型声明称为可选。
class AddItemViewController: UITableViewController, . . . {
// This variable refers to the other view controller
var checklistViewController: ChecklistViewController
@IBAction func done() {
// Create the new checklist item object
let item = ChecklistItem()
item.text = textField.text!
// Directly call a method from ChecklistViewController
checklistViewController.add(item)
}
}
在这种情况下,AddItemViewController 有一个引用 ChecklistViewController 的变量,并且 done() 方法内使用新的 ChecklistItem 对象调用其 add() 方法。
这种方式当然可以工作,但它不是 iOS 中好的实现方式。这种方法的最大缺点是将这两个视图控制器对象捆绑在了一起。
通常,如果屏幕 A 启动了屏幕 B,则我们不希望屏幕 B 对调用它的屏幕 A 了解太多。B 对 A 的了解越少越好。
这时,我们就可以使用代理 (delegate) 了。
delegate 模式
委托模式(delegate pattern)最酷的事情是,屏幕 B 并不真正了解屏幕 A。它只知道某个对象是其委托,但并不真正在乎委托是谁。就像 UITableView 并不真正在乎我们的视图控制器一样,它只是在 table view 需要它们时才提供 table view cells 。
在 AddItemViewController.swift 文件的头部添加如下代码:
protocol AddItemViewControllerDelegate: class {
func addItemViewControllerDidCancel(
_ controller: AddItemViewController)
func addItemViewController(
_ controller: AddItemViewController,
didFinishAdding item: ChecklistItem
)
}
与我们之前看到的方法不同,这些方法中没有实现代码。该协议仅列出方法的名称。
这与协议完全相同,我们可以让一个协议继承于另一个协议,也可以指定可以遵守我们的协议的特定对象类型。
class 关键字标识我们希望 AddItemViewControllerDelegate 协议限于 class 类型。
Protocols
协议通常不会实现其声明的任何方法。它只是说:遵守此协议的任何对象都必须实现方法 X,Y 和 Z。在某些特殊情况下,我们还可能希望为该协议提供默认实现。
调用方法:
@IBAction func cancel() {
delegate?.addItemViewControllerDidCancel(self)
}
@IBAction func done() {
let item = ChecklistItem()
item.text = textField.text!
delegate?.addItemViewController(self, didFinishAdding: item)
}
可选类型 Optionals
Swift 中的变量和常量必须始终有一个 value。在其他编程语言中,特殊符号 nil 或 NULL 通常用于指示变量没有值。而在Swift 中,普通变量是不允许为空的。
nil 和 NULL 的问题在于它们是导致应用崩溃的常见原因。如果某个 app 在不希望使用 nil 变量时尝试使用 nil 变量,则该 app 将 crash。这是可怕的“空指针取消引用”(null pointer dereference)错误。
Swift 通过阻止我们将 nil 与常规变量一起使用来阻止这种情况。
但是,有时变量确实需要 “no value”。这种情况下,我们可以将其设为可选。我们使用问号(?) 或感叹号(!)在 Swift中将某些内容标记为可选内容。
只有设为可选的变量的值才能为 nil。
delegate?.addItemViewControllerDidCancel(self)
这里 ? 告诉 Swift 如果代理为 nil 则不发送消息。我们可以将其读为:“有 delegate 吗? 然后发送消息。” 这种做法称为“可选链接”(optional chaining ),在 Swift 中有着广泛使用。
可选选项在其他编程语言中并不常见,因此我们可能需要慢慢习惯。我们可以发现可选选项确实使程序更清晰 -- 大多数变量都不必为 nil,因此最好防止它们变为nil,避免这些潜在的 bug 来源。
Weak 弱引用
使用弱引用可以避免循环引用(ownership cycle)。
当对象 A 强引用对象 B,同时对象 B 也强引用 A 时,则这两个对象造成循环引用:ownership cycle。
通常,当一个对象不再有其它对象的引用时,该对象将被销毁或释放。但是,由于 A 和 B 相互之间有强关联性,所以它们使彼此保持存活。结果是造成潜在的内存泄漏。
在这种情况下,应该销毁的对象不会被销毁,并且其数据的内存也永远不会被回收。
如果有足够多的此类泄漏,iOS 将耗尽可用内存,从而导致我们的 app crash。这很危险!
delegates 应该总是使用软引用才对。
(还有另一种类型 unowned,它与 weak类似,也可以用于委托。区别在于,weak变量可以再次变为 nil)
@IBOutlets 通常也用 weak 关键字声明。这样做不是为了避免循环引用,而是要明确说明 view controller 实际上并不是views 的所有者。
编辑 Items :新增 checkmark
在 ChecklistViewController.swift 中,修改 configureCheckmark(for:with:) 方法:
func configureCheckmark(
for cell: UITableViewCell,
with item: ChecklistItem
) {
let label = cell.viewWithTag(1001) as! UILabel
if item.checked {
label.text = "√"
} else {
label.text = ""
}
}
在 AddItemViewController.swift 中:
override func viewDidLoad() {
. . .
if let item = itemToEdit {
title = "Edit Item"
textField.text = item.text
}
}
为了使用它,我们首先需要 unwrap the optional。可以使用以下特殊语法进行操作:
if let temporaryConstant = optionalVariable {
// temporaryConstant now contains the unwrapped value of the
// optional variable. temporayConstant is only available from
// within this if block
}
if let itemToEdit = itemToEdit {
title = "Edit Item"
textField.text = itemToEdit.text
}
看起来有点奇怪是不是?
为什么我们又将 itemToEdit 中的值重新分配给itemToEdit?
我们这样编写代码,为什么编译器现在不会警告 optional unwrapping ?
上面的做法称为 variable shadowing - 仅在 if 条件下创建 itemToEdit 变量的“shadow”实例,并且该“shadow”实例是原始可选 itemToEdit 变量的 unwrapped 实例。
因此,在将 text 分配给 text field 时,引用 itemToEdit,实际上是在引用变量的未包装实例(unwrapped instance),而不是原始的可选实例(original optional instance)。
如果你不熟悉 Swift 和 Optionals,这可能会使您感到困惑。因此,是否使用 variable shadowing 完全取决于您。
我更喜欢 shadowing,因为那样代码就始终清楚所引用的变量,因为 optional 和 unwrapped 都使用相同的变量名称。
view controllers 之间传递参数
视图控制器之间的数据传输有两种方式:
从 A 到 B。当屏幕 A 打开屏幕 B 时,A 可以向 B提供所需的数据。我们只需在 B 的视图控制器(view controller)中创建一个新的实例变量,然后,屏幕 A 通常在prepare(for:sender:) ,在屏幕 B 可见之前将一个对象放到 B 的属性中。
从 B 到 A,使用 delegate 将 B 数据传回 A。
在 delegate protocol 中处理编辑事件
AddItemViewController.swift 中,现在 protocol 方法是这样:
protocol AddItemViewControllerDelegate: class {
func addItemViewControllerDidCancel(
_ controller: AddItemViewController)
func addItemViewController(
_ controller: AddItemViewController,
didFinishAdding item: ChecklistItem
)
func addItemViewController(
_ controller: AddItemViewController,
didFinishEditing item: ChecklistItem
)
}
当用户点击“取消”时,一方法被调用;当用户点击“完成”时,有两个方法被调用。
添加新项目后,调用 didFinishAdding,但是在编辑现有项目时,现在应改为调用新的didFinishEditing 方法。
通过使用不同的方法,委托(ChecklistViewController)可以区分这两种情况。
在 AddItemViewController.swift中,将 done() 方法更改为:
@IBAction func done() {
if let item = itemToEdit {
item.text = textField.text!
delegate?.addItemViewController(
self,
didFinishEditing: item)
} else {
let item = ChecklistItem()
item.text = textField.text!
delegate?.addItemViewController(self, didFinishAdding: item)
}
}
Iterative development 迭代开发
不要试图一开始就可以把软件设计的尽善尽美,这是不可能的。刚开始的时候,有比完美重要。
迭代开发,没有设计可以一开始就完美覆盖所有问题,遇到问题,再回头进行优化,一次次迭代,让软件越来越好。你看,市场上所有软件都有版本更新,这就是迭代更新了。
我们要用迭代开发的方法让我们的 app 越来越好。
Saving & Loading
由于 iOS 的多任务处理特性,当我们关闭某个 app 并返回到主屏幕或切换到另一个 app 时,该 app 会存留在内存中。该 app 进入暂停状态,在此状态下,它完全不执行任何操作,但该 app 的内存中的数据还在。
在正常使用期间,用户永远不会真正终止应用程序,而只是将其挂起。但是,当 iOS 的可用内存用尽时,该 app 将被终止,因为 iOS 会终止所有已暂停的应用,以便在必要时释放内存。
documents 文件夹
iOS app 使用沙盒机制。每个应用程序都有自己的文件夹来存储文件,但无法访问其他 app 的目录或文件。
这是一种安全措施,旨在防止恶意软件(如病毒)造成损害。
我们的 app 可以将文件存储在应用程序沙盒的“Documents” 文件夹中。
当用户将其设备与 iTunes 或 iCloud 同步时,将备份 Documents 文件夹中的内容。
当我们发布 app 的新版本并且用户安装更新时,Documents文件夹将保持不变。app 更新后,该 app 之前保存到此文件夹中的所有数据仍然存在。
也就是说,Documents 文件夹是存储用户数据的理想场所。
获取文件保存路径
在 ChecklistViewController.swift 文件添加如下代码:
func documentsDirectory() -> URL {
let paths = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask)
return paths[0]
}
func dataFilePath() -> URL {
return documentsDirectory().appendingPathComponent("Checklists.plist")
}
documentsDirectory() 方法返回 Documents 文件夹完整路径。
dataFilePath() 方法调用 documentsDirectory() 获取 documentDirectory 目录,并在该目录下拼接文件路径 /documentDirectory/Checklists.plist。
网站使用 http:// 或 https:// 表示 URLs。iOS 则使用 file:// URL 这样的 URL 来引用其文件系统中的文件。
在 ChecklistViewController.swift 的 viewDidLoad 的底部添加如下两个打印语句:
override func viewDidLoad() {
. . .
items.append(item5)
// Add the following
print("Documents folder is \(documentsDirectory())")
print("Data file path is \(dataFilePath())")
}
运行 app 。Xcode 的控制台现在将打印出 app 的 Documents 文件夹的所在路径。
如果我从模拟器运行该应用程序,则在我的系统上它将显示以下内容:
如果在 iPhone 真机上运行 app ,则路径看起来会有所不同:
Documents folder is file:///var/mobile/Applications/FDD50B54-9383-4DCC-9C19-C3DEBC1A96FE/Documents
Data file path is file:///var/mobile/Applications/FDD50B54-9383-4DCC-9C19-C3DEBC1A96FE/Documents/Checklists.plist
我们注意到,文件夹名是一个随机的 32 个字符的ID。Xcode 在将 app 安装在 Simulator 或设备上时会记住此ID。
Browse the documents folder
我们在模拟器运行 app,在 mac Finder 我们可以轻松访问到 Documents folder:
通过在桌面上单击并键入⌘+ N,打开一个新的Finder 窗口。或者通过单击 Finder 图标,然后按⌘+ Shift + G-或从菜单中选择“前往文件夹...”,从Xcode Console 复制 Documents 文件夹路径,然后将完整路径粘贴到对话框中的Documents文件夹。 (不包括 file:// 位。路径以/Users/<yourname>/… 开头)
效果如下:
我们还可以查看真机设备上 app “ Documents” 文件夹的概述(overview):
在真机上,转到“设置”,然后单击“iPhone存储空间”,点击我们 app 名称,就可以看到 Documents 文件夹中内容的大小,但看不到它的存储内容:
保存 checklist items
添加新项目或编辑现有项目时,将待办事项列表保存到名为 Checklists.plist 的文件中。
Plist files 序列化
Info.plist 包含几个配置选项,这些选项可为 iOS 提供有关该 app 的一些信息,例如在主屏幕上该 app 图标下显示的名称。
“ plist”代表“属性列表”(Property Lis),它是一种 XML 文件格式,用于存储结构化数据,通常用来配置“设置”所需的信息。属性列表文件在 iOS 中非常常见,因为它们易于使用,它们适用于多种类型的数据存储。
要保存 checklist items,我们需要使用 Swift 的 Codable协议,该协议允许支持 Codable 协议的对象以结构化文件格式存储在磁盘。
在 Objective-C 中,Xcode 使用 NSCoder 将对象写入文件,即编码,当 app 启动时,它再次使用 NSCoder 从 storyboard 文件中读取对象,即解码。
可编码协议的工作原理与此类似。
将对象转换为文件然后再次转换的过程也称为序列化。
保存数据到文件
ChecklistViewController.swift:
func saveChecklistItems() {
let encoder = PropertyListEncoder()
do {
let data = try encoder.encode(items)
try data.write(
to: dataFilePath(),
options: Data.WritingOptions.atomic)
} catch {
print("Error encoding item array: \(error.localizedDescription)")
}
}