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 属性。如果未来有一种鸟(比如鸵鸟)不能飞但有羽毛,我们只需让它遵循 BirdFeatherable 协议即可,设计非常灵活。


本节课中我们一起学习了Swift中的面向协议编程。

我们首先了解了POP的基本概念及其与OOP的主要区别:OOP围绕对象构建,而POP围绕协议构建。接着,我们通过一个“鸟”的建模示例,对比了两种范式。在OOP中,通过继承导致的“紧耦合”使得像企鹅这样的特例难以优雅处理。而在POP中,我们通过定义细粒度的协议(BirdFlyableFeatherable)并利用协议组合扩展,让每个类型(如 ParrotEaglePenguin)只声明自己真正拥有的能力,实现了“松耦合”和更高的灵活性。

面向协议编程的核心优势在于它鼓励组合而非继承,使代码更模块化、更易于测试和维护,并能更好地利用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 拖拽到代码中,创建一个名为 fruitNameLabelIBOutlet
    • 将单元格中的 UIButton 拖拽到代码中,创建一个名为 favoriteButtonIBOutlet
    • 同样为这个按钮创建一个 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. 最后,在 ViewControllerviewDidLoad 方法中,将表格视图的 dataSource 属性设置为 self
override func viewDidLoad() {
    super.viewDidLoad()
    favoriteTableView.dataSource = self
}

添加资源图片

为了区分收藏状态,我们需要两张图片:一颗实心星星(收藏)和一颗空心星星(未收藏)。请确保已将这两张图片(例如命名为 favouritenot_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 ViewTable 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 方法中,将表格视图的 delegatedataSource 设置为 self,并调用 reloadData() 方法。

override func viewDidLoad() {
    super.viewDidLoad()
    displayList.delegate = self
    displayList.dataSource = self
    displayList.reloadData()
}

然后,通过扩展来实现 UITableViewDelegateUITableViewDataSource 协议。

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

DisplayFavoriteListViewControllerviewDidLoad 中,设置导航栏标题。

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  阅读(10)  评论(0)    收藏  举报