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.”) // 被迫重写,设计不优雅
    }
}

上一节我们看到了面向对象编程在应对某些场景时的局限性,本节中我们来看看如何使用面向协议编程优雅地解决这个问题。

面向协议编程的核心是将设计从“类”转变为“协议”和“结构体”。我们不再从一个庞大的基类开始,而是定义一系列细粒度的协议。

首先,我们定义三个协议,分别描述鸟的不同特征:

  1. Bird 协议:定义所有鸟都有的基本属性,如名字。
  2. Flyable 协议:定义可飞行的能力。
  3. Featherable 协议:定义拥有羽毛的特性。

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

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/educba-pop-swift/img/c508778d35a67dc5cd9847f5c8e306ca_5.png)

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:主故事板文件,用于设计用户界面。

设计主界面

接下来,我们在故事板中设计主界面。

  1. 打开 Main.storyboard,从对象库中拖拽一个 UITableView 到默认的 ViewController 场景中。
  2. 使用自动布局约束,让表格视图填满整个安全区域。
  3. 在表格视图中,我们需要一个自定义的单元格来显示每种水果。从对象库中拖拽一个 UITableViewCell 到表格视图内。
  4. 在这个自定义单元格中,我们添加两个UI组件:
    • 一个 UILabel,用于显示水果名称。将其字体大小调整为20,并设置一个背景色以便区分。
    • 一个 UIButton,作为“收藏”按钮。调整其大小,并设置一个背景色。

为了管理这个自定义单元格,我们需要为其创建一个对应的Swift类。

创建自定义单元格类

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

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

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

配置视图控制器

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

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

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

  1. 为了让表格视图显示数据,我们需要让 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
    }
}
  1. 最后,在 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 的逻辑同步。

添加导航按钮并传递数据

最后,我们需要一个按钮来跳转到显示“最爱水果列表”的视图控制器。

  1. 在故事板中,向 FruitListViewController 添加一个按钮,例如命名为“发送水果列表”。
  2. 为按钮创建一个 @IBAction 方法。
  3. 在该方法中,执行跳转(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 中添加一个新的视图控制器。

以下是具体步骤:

  1. 打开 Main.storyboard 文件。
  2. 从对象库中拖拽一个 View Controller 到画布上。
  3. 在新添加的视图控制器中,拖入一个 Table View。
  4. 在 Table View 内部,添加一个 Table View Cell。
  5. 在 Table View Cell 中,添加一个 Label 用于显示水果名称。

由于 Table View 是 Table View Cell 的容器,我们需要为其设置约束以确保布局正确。

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


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

界面搭建完成后,我们需要创建对应的 Swift 类来管理逻辑。

以下是需要创建的类:

  1. DisplayFavoriteListViewController:这是一个 UIViewController 的子类,将作为新视图控制器的类。
  2. 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()
}


调试与优化

运行应用时,如果发现数据没有正确显示,需要进行调试。

常见的检查点包括:

  1. 确保表格单元格的 Identifier 在故事板和代码中完全一致。
  2. 在数据传递的关键位置(如 cellForRowAt 方法)设置断点,检查 receivedFruitNames 数组是否包含数据。
  3. 使用 guard let 安全地解包单元格是良好的实践,可以避免因标识符错误导致的崩溃。

通过使用 guard let,即使未能获取到正确的单元格,也会返回一个默认的 UITableViewCell,从而防止应用崩溃。


总结

本节课中我们一起学习了如何构建一个显示收藏列表的功能。

我们主要完成了以下三件事:

  1. 创建收藏列表界面:在故事板中添加新的视图控制器和表格视图,并创建对应的类。
  2. 实现数据传递:通过模型类(FruitModel)和属性,将用户在主界面选择的水果数据传递到新界面。
  3. 处理导航与状态:设置导航控制器,实现界面跳转,并在返回主界面时清空状态,确保用户体验的连贯性。

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

posted @ 2026-03-29 09:25  布客飞龙II  阅读(19)  评论(0)    收藏  举报