利用 Identifiable 的資料判斷點選的 button 在哪個 cell

操作 iOS App 時,我們時常在表格的 cell 上點擊按鈕, 比方下圖 App Store App 的價錢按鈕。不過當使用者點擊價錢時,我們要如何從程式判斷他想購買哪個 App 呢 ?

判斷的方法很多,其中一招是利用轉換座標的 convert(_:to:) function 判斷點選的 button 在第幾個 cell。

以下我們介紹另一個方法,此方法主要參考 Apple 的 Today App 範例,利用 Identifiable 的資料判斷點選的 button 在哪個 cell。

顯示 App 清單

在示範判斷的方法前,我們先快速做個顯示 App 清單的畫面。

  • storyboard 畫面設計。

設定 auto layout,button 的 Horizontal Content Hugging 設為 252。

  • 定義資料型別 App。

遵從 protocol Identifiable,讓 App 成為可用 id 識別區分的資料。

App.swift。

import Foundation

struct App: Identifiable {
let id = UUID()
let name: String
}
  • 定義 AppTableViewCell & 連結 outlet。
import UIKit

class AppTableViewCell: UITableViewCell {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var iconImageView: UIImageView!

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}
}
  • 定義 AppTableViewController。
class AppTableViewController: UITableViewController {
let apps = [
App(name: "IG"),
App(name: "LINE")
]

override func viewDidLoad() {
super.viewDidLoad()
}

// MARK: - Table view data source

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return apps.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppTableViewCell", for: indexPath) as! AppTableViewCell

let app = apps[indexPath.item]
cell.nameLabel.text = app.name
cell.iconImageView.image = UIImage(named: app.name)
return cell
}

}

App 執行的畫面如下。

利用 Identifiable 的資料判斷點選的 button 在哪個 cell

  • 定義 buy button 的類別。

宣告儲存 App id 的 property id,之後我們將用它找出對應的 App。型別 App.ID 的 ID 是 Identifiable 的 associatedtype,它將等於 UUID,因為我們在 App 裡宣告的 id 型別是 UUID。

class BuyButton: UIButton {
var id: App.ID?
}
  • 設定 buy button 的類別。
  • 連結 buy button 的 outlet。
class AppTableViewCell: UITableViewCell {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var buyButton: BuyButton!
@IBOutlet weak var iconImageView: UIImageView!
  • 設定 buy button 的 id。

將 buy button 的 id 設為 app 的 id。

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppTableViewCell", for: indexPath) as! AppTableViewCell

let app = apps[indexPath.item]
cell.nameLabel.text = app.name
cell.iconImageView.image = UIImage(named: app.name)
cell.buyButton.id = app.id
return cell
}
  • 定義找出成員在 array 裡 index 的 function。

function indexOfElement 接收的參數為成員的 id ,它將回傳成員在 array 裡的 index。

Array+Extensions.swift。

extension Array where Element: Identifiable {
func indexOfElement(with id: Element.ID) -> Self.Index {
guard let index = firstIndex(where: { $0.id == id }) else {
fatalError()
}
return index
}
}
  • 定義 buy button 連結的 IBAction & 購買 App 的 function。

連結的方法有以下三種,以下我們示範兩種方法。

方法1: 連到 controller 的 class。

AppTableViewController.swift。

func buy(app: App) {
print("buy \(app.name)")
}

@IBAction func buyButtonTap(_ sender: BuyButton) {
guard let id = sender.id else { return }
let index = apps.indexOfElement(with: id)
let app = apps[index]
buy(app: app)
}

說明

guard let id = sender.id else { return }

從按鈕 sender 讀出 App 的 id。

let index = apps.indexOfElement(with: id)

利用 indexOfElement 找出 array 裡的 index。

let app = apps[index]

從 apps[index] 讀出對應的 app。

方法2: 連到 cell 的 class。

此做法比較複雜,cell 要另外搭配 delegate 跟 controller 溝通。

AppTableViewCell.swift。

import UIKit

protocol AppTableViewCellDelegate: AnyObject {
func buyButtonTap(sender: BuyButton)
}

class AppTableViewCell: UITableViewCell {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var buyButton: BuyButton!
@IBOutlet weak var iconImageView: UIImageView!
weak var delegate: AppTableViewCellDelegate?

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}

@IBAction func buyButtonTap(_ sender: BuyButton) {

delegate?.buyButtonTap(sender: sender)
}
}

點擊購買按鈕時,呼叫 delegate 的 buyButtonTap。

AppTableViewController.swift。

class AppTableViewController: UITableViewController {
let apps = [
App(name: "IG"),
App(name: "LINE")
]

override func viewDidLoad() {
super.viewDidLoad()
}

// MARK: - Table view data source

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return apps.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppTableViewCell", for: indexPath) as! AppTableViewCell

let app = apps[indexPath.item]
cell.nameLabel.text = app.name
cell.iconImageView.image = UIImage(named: app.name)
cell.buyButton.id = app.id
cell.delegate = self
return cell
}

func buy(app: App) {
print("buy \(app.name)")
}

}

定義 buy(app:),在 tableView(_:cellForRowAt:) 設定 cell 的 delegate 為 controller。

extension AppTableViewController: AppTableViewCellDelegate {

func buyButtonTap(sender: BuyButton) {
guard let id = sender.id else { return }
let index = apps.indexOfElement(with: id)
let app = apps[index]
buy(app: app)
}
}

遵從 AppTableViewCellDelegate,定義 buyButtonTap(sender:)。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com