Swift-面向协议编程笔记-全-
Swift 面向协议编程笔记(全)
001:面向协议编程导论 🐦
在本节课中,我们将要学习Swift中一个非常重要的概念——面向协议编程。我们将通过对比面向对象编程,来理解面向协议编程的核心思想、优势以及基本用法。
面向协议编程,通常简称为POP,是苹果公司在2015年WWDC大会上随Swift 2.0引入的新范式。它与我们熟知的面向对象编程有所不同。在面向对象编程中,我们设计和关注的是类及其实例(即对象),对象可以包含数据、属性和方法实现。而在面向协议编程中,我们则从协议开始构建程序。
面向协议编程可以看作是面向对象编程的一种替代方案,并且在Swift中具有许多优势。如今,许多人更倾向于结合结构体使用面向协议编程,而非使用基于引用类型的类。结构体基于值类型,这有助于避免内存泄漏和隐式共享数据带来的多线程问题。
上一节我们介绍了面向协议编程的基本概念,本节中我们来看看一个具体的例子,通过对比两种编程范式来加深理解。
首先,我们使用面向对象编程的方式来建模“鸟”这个类别。

我们创建一个 Bird 类,它拥有两个属性:name(名字)和 feather(羽毛),以及一个 fly(飞行)方法。
class Bird {
var name: String
var feather: String
init(name: String, feather: String) {
self.name = name
self.feather = feather
}
func fly() {
// 飞行行为
}
}
接着,我们创建两个子类:Parrot(鹦鹉)和 Eagle(鹰)。它们继承自 Bird 类,并重写了初始化方法。
class Parrot: Bird {
override init(name: String, feather: String) {
super.init(name: name, feather: feather)
}
override func fly() {
// 鹦鹉的飞行行为
}
}
class Eagle: Bird {
override init(name: String, feather: String) {
super.init(name: name, feather: feather)
}
override func fly() {
// 鹰的飞行行为
}
}
现在,问题出现了。当我们想添加一个 Penguin(企鹅)类时,我们发现企鹅是鸟,但它没有羽毛且不能飞。在面向对象编程中,由于继承了 Bird 类,我们被迫拥有 feather 属性和 fly 方法,即使它们对企鹅不适用。我们只能通过重写 fly 方法并给出“我不能飞”这样的实现来勉强解决,但这破坏了设计,并且如果子类众多,会带来大量的重复代码。
class Penguin: Bird {
override init(name: String, feather: String) {
super.init(name: name, feather: feather)
}
override func fly() {
print(“I cannot fly.”) // 被迫重写,设计不优雅
}
}
上一节我们看到了面向对象编程在应对某些场景时的局限性,本节中我们来看看如何使用面向协议编程优雅地解决这个问题。
面向协议编程的核心是将设计从“类”转变为“协议”和“结构体”。我们不再从一个庞大的基类开始,而是定义一系列细粒度的协议。
首先,我们定义三个协议,分别描述鸟的不同特征:
Bird协议:定义所有鸟都有的基本属性,如名字。Flyable协议:定义可飞行的能力。Featherable协议:定义拥有羽毛的特性。

protocol Bird {
var name: String { get set }
}

protocol Flyable {
func fly()
}
protocol Featherable {
var feather: String { get set }
}
我们可以为协议提供默认实现,这通过扩展来完成。例如,为 Flyable 协议提供一个通用的 fly 方法实现。
extension Flyable {
func fly() {
print(“I can fly.”)
}
}
extension Featherable {
var feather: String {
get { return “” }
set { }
}
}
现在,我们可以通过组合不同的协议来创建具体的类型。以下是使用结构体和协议组合的例子:
对于鹦鹉和鹰,它们具有全部三种特征。
struct Parrot: Bird, Flyable, Featherable {
var name: String
var feather: String
init(name: String) {
self.name = name
self.feather = “Colorful feathers”
}
}
struct Eagle: Bird, Flyable, Featherable {
var name: String
var feather: String
init(name: String) {
self.name = name
self.feather = “Brown feathers”
}
}
对于企鹅,它只是一只鸟,既不能飞也没有羽毛。因此,我们只让它遵循 Bird 协议。
struct Penguin: Bird {
var name: String
init(name: String) {
self.name = name
}
}
通过这种方式,Penguin 结构体非常简洁,只包含了它确实拥有的属性。它没有被迫实现 fly 方法或 feather 属性。如果未来有一种鸟(比如鸵鸟)不能飞但有羽毛,我们只需让它遵循 Bird 和 Featherable 协议即可,设计非常灵活。
本节课中我们一起学习了Swift中的面向协议编程。
我们首先了解了POP的基本概念及其与OOP的主要区别:OOP围绕类和对象构建,而POP围绕协议构建。接着,我们通过一个“鸟”的建模示例,对比了两种范式。在OOP中,通过继承导致的“紧耦合”使得像企鹅这样的特例难以优雅处理。而在POP中,我们通过定义细粒度的协议(Bird, Flyable, Featherable)并利用协议组合与扩展,让每个类型(如 Parrot, Eagle, Penguin)只声明自己真正拥有的能力,实现了“松耦合”和更高的灵活性。

面向协议编程的核心优势在于它鼓励组合而非继承,使代码更模块化、更易于测试和维护,并能更好地利用Swift值类型(如结构体)的优势。在实际项目中,应根据具体需求决定是否采用POP。理解并善用这一范式,将帮助你写出更清晰、更强大的Swift代码。
002:显示水果列表 🍎
在本节课中,我们将学习如何使用委托模式和模型类创建一个“最爱水果列表”应用。我们将构建一个显示水果列表的界面,并允许用户选择他们喜欢的水果。被选中的水果将保存在模型类中,并在另一个视图控制器中以表格形式显示。
上一节我们介绍了项目的基本设置,本节中我们来看看如何构建显示水果列表的界面。

创建项目与界面


首先,我们创建一个新的Xcode项目。选择“Single View App”模板,将项目命名为“FavouriteList”。在项目设置中,确保界面选项选择了“Storyboard”,并取消勾选“Use Core Data”、“Include Unit Tests”和“Include UI Tests”。
项目创建完成后,我们会在项目导航器中看到以下主要文件:
AppDelegate.swift:处理应用程序生命周期。ViewController.swift:主视图控制器类。Main.storyboard:主故事板文件,用于设计用户界面。
设计主界面
接下来,我们在故事板中设计主界面。
- 打开
Main.storyboard,从对象库中拖拽一个UITableView到默认的ViewController场景中。 - 使用自动布局约束,让表格视图填满整个安全区域。
- 在表格视图中,我们需要一个自定义的单元格来显示每种水果。从对象库中拖拽一个
UITableViewCell到表格视图内。 - 在这个自定义单元格中,我们添加两个UI组件:
- 一个
UILabel,用于显示水果名称。将其字体大小调整为20,并设置一个背景色以便区分。 - 一个
UIButton,作为“收藏”按钮。调整其大小,并设置一个背景色。
- 一个
为了管理这个自定义单元格,我们需要为其创建一个对应的Swift类。


创建自定义单元格类

以下是创建自定义单元格类的步骤:


- 在Xcode中,右键点击项目导航器,选择“New Group”,创建一个名为“Cell Class”的组。
- 在该组内,选择“File” -> “New” -> “File…”,创建一个新的Cocoa Touch Class。
- 将类命名为
FavoriteCell,并确保其继承自UITableViewCell。 - 创建完成后,回到故事板。选中我们设计的自定义单元格,在身份检查器中将它的类设置为
FavoriteCell,在属性检查器中将它的重用标识符也设置为FavoriteCell。 - 现在,我们需要将故事板中的UI组件与
FavoriteCell类中的属性和方法连接起来。打开辅助编辑器,确保FavoriteCell.swift文件在右侧显示。- 将单元格中的
UILabel拖拽到代码中,创建一个名为fruitNameLabel的IBOutlet。 - 将单元格中的
UIButton拖拽到代码中,创建一个名为favoriteButton的IBOutlet。 - 同样为这个按钮创建一个
IBAction,命名为favoriteButtonAction。我们稍后将通过这个动作来触发委托。
- 将单元格中的

连接完成后,我们可以将单元格和按钮的背景色恢复为白色,并清空标签的默认文本。

配置视图控制器

现在,我们需要在主视图控制器中配置表格视图。

- 首先,在
ViewController.swift中,为故事板中的表格视图创建一个IBOutlet,命名为favoriteTableView。 - 接着,我们需要一个数据源来填充表格。在
ViewController类中,定义一个字符串数组fruitListArray,并初始化一些水果名称。


var fruitListArray: [String] = ["Apple", "Mango", "Banana", "Cherry", "Grapes"]

- 为了让表格视图显示数据,我们需要让
ViewController遵守UITableViewDataSource协议。通常我们使用扩展来实现协议,使代码结构更清晰。
在 ViewController.swift 文件末尾添加以下扩展:
extension ViewController: UITableViewDataSource {
// 返回表格的行数,等于水果数组的元素个数
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruitListArray.count
}
// 配置并返回每一行的单元格
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 通过重用标识符获取自定义单元格实例
guard let cell = tableView.dequeueReusableCell(withIdentifier: "FavoriteCell", for: indexPath) as? FavoriteCell else {
return UITableViewCell()
}
// 设置单元格标签的文本为对应位置的水果名称
cell.fruitNameLabel.text = fruitListArray[indexPath.row]
// 将按钮的tag设置为当前行号,便于后续识别哪个按钮被点击
cell.favoriteButton.tag = indexPath.row
// 为按钮设置默认的“未收藏”状态图片
cell.favoriteButton.setImage(UIImage(named: "not_favourite"), for: .normal)
return cell
}
}
- 最后,在
ViewController的viewDidLoad方法中,将表格视图的dataSource属性设置为self。
override func viewDidLoad() {
super.viewDidLoad()
favoriteTableView.dataSource = self
}


添加资源图片
为了区分收藏状态,我们需要两张图片:一颗实心星星(收藏)和一颗空心星星(未收藏)。请确保已将这两张图片(例如命名为 favourite 和 not_favourite)添加到项目的资源目录(Assets.xcassets)中。
运行与预览
至此,显示水果列表的基础功能已经完成。运行应用,你应该能看到一个表格,其中列出了我们在数组中定义的所有水果名称,并且每一行旁边都有一个显示为空心星星的按钮。

本节课中我们一起学习了如何创建Swift项目、使用故事板设计包含表格视图的界面、创建自定义表格单元格以及配置表格的数据源。在下一节中,我们将为“收藏”按钮添加交互逻辑,实现状态的切换和数据保存。
003:逻辑与偏好选择
在本节课中,我们将为水果列表视图控制器实现逻辑部分。我们将处理“我的最爱”按钮的点击事件,改变星星图标的颜色,并创建一个模型类来存储用户选择的水果名称,以便在下一个视图控制器中显示。
创建协议与委托
上一节我们完成了界面布局,本节中我们来看看如何通过协议和委托模式来处理按钮的交互逻辑。
首先,在自定义单元格类 FruitCell 中创建一个协议。协议本质上是一组方法的蓝图,任何采纳该协议的类都必须实现这些方法,从而获得相应的功能。
以下是协议的定义:
protocol FavoriteDelegate {
func favoriteTapped(at index: Int)
}
在单元格类中,我们创建一个该协议的委托对象:
var delegate: FavoriteDelegate?
接下来,为“最爱”按钮的点击事件设置目标动作。当按钮被点击时,通过委托将按钮的标签(即单元格的索引)传递出去。
@IBAction func favoriteButtonTapped(_ sender: UIButton) {
delegate?.favoriteTapped(at: sender.tag)
}
在视图控制器中实现委托
现在,我们需要在主视图控制器中采纳并实现这个协议,以响应单元格的按钮点击。
首先,让视图控制器采纳 FavoriteDelegate 协议:
class FruitListViewController: UIViewController, FavoriteDelegate {
// ... 其他代码
}
在 viewDidLoad 方法中,将表格视图的单元格委托设置为 self:
override func viewDidLoad() {
super.viewDidLoad()
// 假设你的tableView变量名为tableView
// 通常在配置cell时设置delegate,这里是一个概念性步骤
}
然后,实现协议要求的方法 favoriteTapped(at:)。在这个方法里,我们需要处理用户的选择状态。
管理选择状态与数据模型
为了跟踪哪些水果被用户标记为“最爱”,我们需要一个可变的数据结构来存储状态。这里使用 NSMutableDictionary,因为它允许我们动态地添加和移除条目。

在视图控制器中声明一个字典:
var favoriteDictionary = NSMutableDictionary()
在 favoriteTapped(at:) 方法中,我们根据索引更新这个字典和模型数据:
func favoriteTapped(at index: Int) {
let key = String(index)
// 检查该水果是否已被收藏
if let _ = favoriteDictionary.value(forKey: key) {
// 如果已存在,则移除(取消收藏)
favoriteDictionary.removeObject(forKey: key)
// 同时从模型数组中移除对应水果
// 假设 fruitModelArray 是存储 FruitModel 的数组
// fruitModelArray.remove(at: index) // 注意:实际索引可能需要转换
} else {
// 如果不存在,则添加(收藏)
favoriteDictionary.setValue(index, forKey: key)
// 同时将水果添加到模型数组
// let fruitName = fruitNamesArray[index] // 假设有一个水果名称数组
// let newFruit = FruitModel(name: fruitName)
// fruitModelArray.append(newFruit)
}
// 刷新表格视图以更新按钮图标
tableView.reloadData()
}
在配置单元格的 cellForRowAt 方法中,根据 favoriteDictionary 来设置按钮的显示状态(例如,实心星或空心星)。
创建数据模型
为了将选中的水果列表传递到下一个界面,我们需要一个数据模型。创建一个名为 FruitModel 的类。
以下是模型类的定义:
class FruitModel {
var fruitName: String
init(fruitName: String) {
self.fruitName = fruitName
}
}
在视图控制器中,声明一个 FruitModel 类型的数组来存储被选中的水果:
var selectedFruits = [FruitModel]()


在 favoriteTapped(at:) 方法中更新这个数组,与更新 favoriteDictionary 的逻辑同步。
添加导航按钮并传递数据
最后,我们需要一个按钮来跳转到显示“最爱水果列表”的视图控制器。

- 在故事板中,向
FruitListViewController添加一个按钮,例如命名为“发送水果列表”。 - 为按钮创建一个
@IBAction方法。 - 在该方法中,执行跳转(Segue),并在
prepare(for:sender:)方法中将selectedFruits数组传递给目标视图控制器。

以下是跳转前准备数据的方法:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowFavoriteFruits" {
let destinationVC = segue.destination as! FavoriteFruitsViewController
destinationVC.favoriteFruits = self.selectedFruits
}
}
总结

本节课中我们一起学习了如何为Swift应用实现交互逻辑与数据管理。我们首先创建了协议和委托来处理单元格内的按钮点击,然后使用 NSMutableDictionary 来管理用户的选择状态。接着,我们构建了 FruitModel 数据模型来结构化存储数据,并最终通过Segue将用户选择的“最爱水果”列表传递到新的视图控制器进行展示。通过这一系列步骤,我们完成了从用户交互到数据持久化再到界面跳转的完整逻辑链。
004:显示收藏列表 🍎



在本节课中,我们将学习如何在一个新的视图控制器中显示用户选择的“最爱水果”列表。我们将通过创建新的界面、设置表格视图、传递数据模型以及实现导航来完成这个功能。

创建新视图控制器与界面

上一节我们介绍了如何收集用户的选择。本节中,我们来看看如何创建一个新的界面来展示这些选择。


首先,我们需要在 Main.storyboard 中添加一个新的视图控制器。

以下是具体步骤:
- 打开
Main.storyboard文件。 - 从对象库中拖拽一个
View Controller到画布上。 - 在新添加的视图控制器中,拖入一个
Table View。 - 在
Table View内部,添加一个Table View Cell。 - 在
Table View Cell中,添加一个Label用于显示水果名称。
由于 Table View 是 Table View Cell 的容器,我们需要为其设置约束以确保布局正确。


接下来,为 Table View Cell 中的 Label 设置约束,使其在单元格内正确显示。

创建视图控制器与单元格类

界面搭建完成后,我们需要创建对应的 Swift 类来管理逻辑。
以下是需要创建的类:
- DisplayFavoriteListViewController:这是一个
UIViewController的子类,将作为新视图控制器的类。 - DisplayCell:这是一个
UITableViewCell的子类,将作为自定义表格单元格的类。
为 DisplayFavoriteListViewController 类命名时,应使用有意义的名称,以便其他开发者理解其用途。
在故事板中,将新视图控制器的 Class 设置为 DisplayFavoriteListViewController,并将其表格单元格的 Class 设置为 DisplayCell,同时设置相同的 Identifier。

设置表格视图数据源与代理

现在,我们需要在 DisplayFavoriteListViewController 中设置表格视图,并准备显示数据。
首先,在 DisplayFavoriteListViewController 中创建一个 FruitModel 类型的数组属性,用于接收从上一个界面传递过来的数据。
var receivedFruitNames: [FruitModel] = []
接着,在 viewDidLoad 方法中,将表格视图的 delegate 和 dataSource 设置为 self,并调用 reloadData() 方法。
override func viewDidLoad() {
super.viewDidLoad()
displayList.delegate = self
displayList.dataSource = self
displayList.reloadData()
}
然后,通过扩展来实现 UITableViewDelegate 和 UITableViewDataSource 协议。
extension DisplayFavoriteListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return receivedFruitNames.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: “DisplayCell”, for: indexPath) as? DisplayCell else {
return UITableViewCell()
}
cell.fruitNameLabel.text = receivedFruitNames[indexPath.row].fruitName
return cell
}
}
在 DisplayCell 类中,为 Label 创建一个 IBOutlet 连接,以便在代码中设置其文本。

实现界面间导航与数据传递


数据展示的逻辑已经完成,现在需要实现从主界面到收藏列表的导航。
首先,为主视图控制器嵌入一个 Navigation Controller。在故事板中,选择主 View Controller,然后点击 Editor -> Embed In -> Navigation Controller。
在 DisplayFavoriteListViewController 的 viewDidLoad 中,设置导航栏标题。


self.title = “Favorite Fruit List”

回到主视图控制器,在“发送收藏列表”按钮的 @IBAction 方法中,实现导航跳转和数据传递。
@IBAction func sendFavoriteTapped(_ sender: Any) {
if favoriteList.count > 0 { // 检查是否有选中的水果
let storyboard = UIStoryboard(name: “Main”, bundle: nil)
if let displayVC = storyboard.instantiateViewController(withIdentifier: “DisplayFavoriteList”) as? DisplayFavoriteListViewController {
displayVC.receivedFruitNames = Array(favoriteList.values) // 传递数据
self.navigationController?.pushViewController(displayVC, animated: true)
}
}
}


同时,当用户返回主界面时,需要清空临时存储的收藏数据,以便下次重新选择。


override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
favoriteList.removeAll() // 清空字典
fruitModelArray.removeAll() // 清空模型数组
self.tableView.reloadData()
}


调试与优化
运行应用时,如果发现数据没有正确显示,需要进行调试。
常见的检查点包括:
- 确保表格单元格的
Identifier在故事板和代码中完全一致。 - 在数据传递的关键位置(如
cellForRowAt方法)设置断点,检查receivedFruitNames数组是否包含数据。 - 使用
guard let安全地解包单元格是良好的实践,可以避免因标识符错误导致的崩溃。
通过使用 guard let,即使未能获取到正确的单元格,也会返回一个默认的 UITableViewCell,从而防止应用崩溃。
总结
本节课中我们一起学习了如何构建一个显示收藏列表的功能。
我们主要完成了以下三件事:
- 创建收藏列表界面:在故事板中添加新的视图控制器和表格视图,并创建对应的类。
- 实现数据传递:通过模型类(
FruitModel)和属性,将用户在主界面选择的水果数据传递到新界面。 - 处理导航与状态:设置导航控制器,实现界面跳转,并在返回主界面时清空状态,确保用户体验的连贯性。

此外,我们还回顾了委托模式(Delegation)的使用,它允许单元格将用户操作(如点击按钮)通知给视图控制器进行处理。通过本教程,你掌握了使用表格视图、委托和模型类来构建一个完整数据流功能的基本方法。

浙公网安备 33010602011771号