#23_TableView 練習-1
任何資料都需要一個表格,如果不行就兩個
此次作業練習來自
比較想要先好好瞭解基本功能,畢竟超不熟的😂,題材的部分就拿了最方便的寶可夢系列,這次有使用到 storyboard,也許第二個作業可以嘗試純 code 製作 🤔,先將作業的內容實現並記下來。
分段(Sectoin & Row)
如果要分出 section 的話,在資料的部分大概要分層會比較方便
舉個例子
struct 移動 {
let 陸海空: String
let 工具: [交通工具]
}
struct 交通工具 {
let 名稱: String
}
var 移動方式: [移動] {
let 汽車 = 交通工具(名稱: "汽車")
let 高鐵 = 交通工具(名稱: "高鐵")
let 陸地交通工具:[交通工具] = [汽車, 高鐵]
let 陸地移動方式 = 移動(陸海空: "陸地", 工具: 陸地交通工具)
let 郵輪 = 交通工具(名稱: "郵輪")
let 游泳 = 交通工具(名稱: "游泳")
let 水上交通工具:[交通工具] = [郵輪, 游泳]
let 水上移動方式 = 移動(陸海空: "水上", 工具: 水上交通工具)
return [ 陸地移動方式, 水上移動方式]
}
這樣就可以使用 移動方式 來作為計算 section 的數量
用 移動方式 裡面的 陸地交通工具 &水上交通工具 作為各自 section 的 row
我在這種資料堆疊和階層方面很腦弱,花了很多時間理解現在在哪 😂
跳回這次的練習,有關 section 和 row 的顯示相關程式碼如以下
// 返回表格視圖中的區段數量,這裡是根據物品陣列的數量決定
override func numberOfSections(in tableView: UITableView) -> Int {
return items.count
}
// 返回每個區段中的行數,這裡是根據該區段物品的數量決定
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items[section].item.count
}
// 設置每個區段的標題
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return items[section].category
}
大概可以簡單的理解成有陣列的地方就可以來分資料吧 🙂,就不知道如果資料不只是兩層那應該怎麼辦 XD
計算屬性 & 靜態屬性
順便看一下這次使用陣列資料的方式
// PokemonItem的擴展,提供一個靜態屬性來生成一個包含所有物品類別和物品的陣列
extension PokemonItem {
// 這個靜態屬性返回一個包含所有預定義物品類別和物品的陣列
static var pokemonItems: [PokemonItem] {
let pokeBall = Item(name: "poke-ball", imageName: "poke-ball", description: "Catches wild Pokémon.")
let greatBall = Item(name: "great-ball", imageName: "great-ball", description: "Catches wild Pokémon with 1.5x the rate of a Poké Ball.")
let ultraBall = Item(name: "ultra-ball", imageName: "ultra-ball", description: "Catches wild Pokémon with 2x the rate of a Poké Ball.")
let beastBall = Item(name: "beast-ball", imageName: "beast-ball", description: "A special Poké Ball designed to catch Ultra Beasts. It has a low success rate for catching others.")
let masterBall = Item(name: "master-ball", imageName: "master-ball", description: "Catches any wild Pokémon without fail.")
let ballCategory = PokemonItem(category: "Ball", item: [pokeBall, greatBall, ultraBall, beastBall, masterBall])
let antidote = Item(name: "antidote", imageName: "antidote", description: "Cures a Pokémon of poisoning.")
let awakening = Item(name: "awakening", imageName: "awakening", description: "Wakes up a sleeping Pokémon.")
let burnHeal = Item(name: "burn-heal", imageName: "burn-heal", description: "Cures a Pokémon of a burn.")
let iceHeal = Item(name: "ice-heal", imageName: "ice-heal", description: "Cures a Pokémon of freezing.")
let fullHeal = Item(name: "full-heal", imageName: "full-heal", description: "Cures a Pokémon of any status condition.")
let fullRestore = Item(name: "full-restore", imageName: "full-restore", description: "Fully restores HP and cures any status condition of a Pokémon.")
let medicineCategory = PokemonItem(category: "Medicine", item: [antidote, awakening, burnHeal, iceHeal, fullHeal, fullRestore])
// 返回包含所有物品類別的陣列
return [ballCategory, medicineCategory ]
}
}
就是在結構中用了靜態屬性和計算屬性雖然計算屬性感覺這裡用不用無所謂
要使用這些資料時只要這樣就好
以這個專案來說好處大概是這個 swift 檔不會太長 XD
// 宣告一個變數來存儲寶可夢物品的陣列
var items = PokemonItem.pokemonItems
Cell 的 ID、呈現、資料傳輸
這次自製了兩種簡單的排版
Autolayout 也是使用 StackView 來做設定,在 StackView 裡面的物件則是有設定 Content Hugging Priority & Content Compression Resistance Priority
我的想像是
Content Hugging Priority:不想變大的指標 ( 數值越大,變大的機率越小)
Content Compression Resistance Priority:不想變小的指標 ( 數值越大,變小的機率越小)
此外上面那兩項的設定也跟 StackView 的 Align & Distri 選擇有關,如果是選到比較接近 StackView 自動計算的話上面那兩樣的作用就不大
可以參考
同一系列的 cell 需要有自己的 ReuseIdentifier,也許可以想像成類似它的 ID 名字,可能我們會有一種以上的 cell,為此在眾多命名 ID 的方式中我選了以下這種
先開一個 Swift 檔,然後
// 為categoryBallTableViewCell添加一個擴展
extension categoryBallTableViewCell {
// 添加一個靜態屬性reuseIdentifier,用於返回該類型的唯一標識符
// 這裡使用"\(Self.self)"是Swift中的一種反射機制,用於獲取當前類型的名稱
// 這樣做可以確保每個UITableViewCell子類都有一個與其類名相同的重用標識符
static var reuseIdentifier: String { "\(Self.self)" }
}
// 為categoryMedicineTableViewCell添加一個擴展,內容與上面相同
extension categoryMedicineTableViewCell {
static var reuseIdentifier: String { "\(Self.self)" }
}
/*
使用這種方式來定義reuseIdentifier有幾個好處:
1.減少錯誤:直接使用類名作為標識符,避免了手動字符串錯誤的可能性。
2.提高可維護性:如果類名發生變化,標識符也會自動更新,不需要手動修改。
3.增強可讀性和一致性:這樣的命名慣例讓代碼更加清晰,也使得在整個項目中尋找和使用這些標識符變得更加容易。
*/
所以就可以在準備呼叫 cell 時給的 ID 名如以下
tableView.dequeueReusableCell(withIdentifier: categoryBallTableViewCell.reuseIdentifier, for: indexPath)
如果是用 storyboard 要記得先在 Identifier 那命名
可以參考
再來就是將陣列裡的資料使用 cell 呈現出來
// 設置每個單元格的內容
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 根據區段和行索引找到對應的物品
let item = items[indexPath.section].item[indexPath.row]
// 根據區段索引來決定單元格的類型
if indexPath.section == 0 {
// if indexPath.section.description == "Ball" {
// 如果是第一個區段,則使用「球」類別的單元格
guard let ballCell = tableView.dequeueReusableCell(withIdentifier: categoryBallTableViewCell.reuseIdentifier, for: indexPath) as? categoryBallTableViewCell else { fatalError("dequeueReusable failed") }
// ballCell.itemNameLabel.text = item.name
// ballCell.itemImageView.image = UIImage(named: item.imageName)
// 更新單元格內容
ballCell.update(item: item)
// 有分 section 的話就需要增加它的 id
ballCell.detailButton.sectionID = items[indexPath.section].id
// 設置委託
ballCell.delegate = self
return ballCell
} else {
// 如果不是第一個區段,則使用「藥品」類別的單元格
guard let medicineCell = tableView.dequeueReusableCell(withIdentifier: categoryMedicineTableViewCell.reuseIdentifier, for: indexPath) as? categoryMedicineTableViewCell else { fatalError("dequeueReusable failed") }
// medicineCell.itemNameLabel.text = item.name
// medicineCell.itemImageView.image = UIImage(named: item.imageName)
// 更新單元格內容
medicineCell.update(item: item)
return medicineCell
}
}
讓資料照著 indexPath.section & indexPath.row 一行行的顯現出來
let item = items[indexPath.section].item[indexPath.row]
區分 section 可以用陣列的 index,或者 section 本身這層是可分辨的文字也可以,我個人感覺要是 section 分很多也許用文字會更好一點?
if indexPath.section == 0 {
// if indexPath.section.description == "Ball" {
使用 guard let 搭配 as? 生成 cell,這裡也是使用到 ID 的地方
// 如果是第一個區段,則使用「球」類別的單元格
guard let ballCell = tableView.dequeueReusableCell(withIdentifier: categoryBallTableViewCell.reuseIdentifier, for: indexPath) as? categoryBallTableViewCell else { fatalError("dequeueReusable failed") }
guard let 可以參考
接著將每行所獲得的資料丟給 cell 之後返回一個有資料的 cell,給予資料的方式可以在這裡給如 ballCell.itemNameLabel.text = item.name
也可以在 Cell 的類別裡寫一個更新資料的 Function
因為我有兩個不同的 cell 所以在這裡用 section == 0
以外的 section 給予第二個 cell 資料
// ballCell.itemNameLabel.text = item.name
// ballCell.itemImageView.image = UIImage(named: item.imageName)
// 更新單元格內容
ballCell.update(item: item)
return ballCell
} else {
// 如果不是第一個區段,則使用「藥品」類別的單元格
guard let medicineCell = tableView.dequeueReusableCell(withIdentifier: categoryMedicineTableViewCell.reuseIdentifier, for: indexPath) as? categoryMedicineTableViewCell else { fatalError("dequeueReusable failed") }
// medicineCell.itemNameLabel.text = item.name
// medicineCell.itemImageView.image = UIImage(named: item.imageName)
// 更新單元格內容
medicineCell.update(item: item)
return medicineCell
}
}
這裡是 Cell 的類別,imageView & label 等等的 IBOulet 也是拉到這裡來,更新資料的方法也寫在這,裡面還有寫一個 protocol 是用來傳遞資料給另一個頁面的,因為藉由點選 cell 或是 cell 裡的按鈕到下一頁面的方式有點多,下面來接著講
// 定義一個協議,用於當單元格中的詳情按鈕被點擊時通知代理執行相應的動作
protocol categoryBallTableViewCellDelegate: AnyObject {
func detailButtonTap(sender: DetailButton)
}
// 自定義表格視圖單元格類別,用於展示寶可夢球類物品
class categoryBallTableViewCell: UITableViewCell {
// UI元件的宣告,包括物品圖像、名稱標籤和詳情按鈕
@IBOutlet weak var itemImageView: UIImageView!
@IBOutlet weak var itemNameLabel: UILabel!
@IBOutlet weak var detailButton: DetailButton!
// 宣告一個代理變數,遵循categoryBallTableViewCellDelegate協議
// 使用weak關鍵字防止循環引用
weak var delegate: categoryBallTableViewCellDelegate?
// 更新單元格內容的方法,根據傳入的Item實例來設置UI元件
func update(item: Item) {
itemImageView.image = UIImage(named: item.imageName)
itemNameLabel.text = item.name
// 將物品的id設置給詳情按鈕的rowID屬性,這樣點擊時可以知道是哪個物品被選擇
detailButton.rowID = item.id
}
// 單元格從故事板載入後的初始化操作
override func awakeFromNib() {
super.awakeFromNib()
// 這裡可以添加一些初始化代碼
}
// 設置單元格選中狀態的方法,這裡可以根據需要來自定義選中效果
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// 這裡可以添加選中或取消選中時的額外處理
}
// 詳情按鈕點擊事件的處理方法,當按鈕被點擊時,通過代理來執行相應的動作
@IBAction func detailButtonTap(_ sender: DetailButton) {
// 通知代理執行detailButtonTap方法
delegate?.detailButtonTap(sender: sender)
}
}
點選 cell 或 cell 裡的按鈕傳資料
有兩個是原本 tableViewController 裡就有但先被註解掉的如以下
兩個差別好像只在有沒有想要在 storyboard 上拉線,下面那種的是不用拉線的
// 為segue準備數據的方法,當觸發segue轉場時調用
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// 檢查segue的標識符是否為"showWithPrepare"
if segue.identifier == "showWithPrepare" {
// 嘗試獲取目標視圖控制器和當前選中的表格行
if let controller = segue.destination as? PokeItemDetailViewController,
let indexPath = tableView.indexPathForSelectedRow {
// 將選中的物品傳遞給詳情視圖控制器
controller.item = items[indexPath.section].item[indexPath.row]
}
}
}
// 處理表格視圖行選中事件的方法
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 獲取選中的物品
let selectItem = items[indexPath.section].item[indexPath.row]
// 實例化詳情視圖控制器
let controller = storyboard?.instantiateViewController(withIdentifier: PokeItemDetailViewController.reuseIdentifier) as! PokeItemDetailViewController
// 設置詳情視圖控制器的物品
controller.item = selectItem
// 以模態方式呈現詳情視圖控制器
present(controller, animated: true)
}
我另外也嘗試了三種,以下是其中的兩種
下面這種其實也是拉線,只是沒有用上 segue.identifier
// 使用IBSegueAction自定義Segue的初始化,使得可以在使用Storyboard Segue時進行更多的自定義設置
@IBSegueAction func showWithSegue(_ coder: NSCoder) -> PokeItemDetailViewController? {
// 獲取當前選中的表格行
guard let indexPath = tableView.indexPathForSelectedRow else { return nil }
let selectItem = items[indexPath.section].item[indexPath.row]
// 用編碼器和選中的物品初始化詳情視圖控制器
let controller = PokeItemDetailViewController(coder: coder)
controller?.item = selectItem
//controller?.update(item: selectItem)
return controller
}
// 通過按鈕觸發的動作,用於顯示詳情視圖控制器
@IBAction func show(_ sender: UIButton) {
// 將按鈕的位置轉換為在表格視圖中的位置
let point:CGPoint = sender.convert(.zero, to: tableView)
// 根據位置獲取對應的indexPath
if let indexPath = tableView.indexPathForRow(at: point) {
// 獲取選中的物品
let selectItem = items[indexPath.section].item[indexPath.row]
// 通過Storyboard實例化詳情視圖控制器
let detailController = storyboard?.instantiateViewController(withIdentifier: PokeItemDetailViewController.reuseIdentifier) as! PokeItemDetailViewController
// 設置詳情視圖控制器的物品
detailController.item = selectItem
// 將詳情視圖控制器推送到導航控制器上
navigationController?.pushViewController(detailController, animated: true)
}
}
上面這種事製作一個按鈕並採取它的座標藉以比對它在表格上的 indexPath
詳細可以參考
但我私自覺得最應該熟悉的是以下這種藉由每項資料的獨有 ID 來做辨認並傳遞資料的方法是最好的( 但也最麻煩 )
首先要先在每筆資料上紀錄 ID,如果有 section 那 section 那一層也要有 ID
遵從 protocol Identifiable
// 定義一個遵循Identifiable協議的PokemonItem結構體,用於表示一個物品類別
struct PokemonItem: Identifiable {
var id = UUID() // 唯一標識符,遵循Identifiable協議必須
let category: String // 物品類別的名稱
var item: [Item] // 屬於該類別的物品陣列
}
// 定義一個遵循Identifiable協議的Item結構體,用於表示單個物品
struct Item: Identifiable {
var id = UUID() // 唯一標識符,同上
let name: String // 物品的名稱
let imageName: String // 物品圖片的名稱
let description: String // 物品的描述
let otherLangurage: String // 物品名稱和描述的其他語言版本
}
自行定義一個按鈕的類別,用來儲存資料獨有的 ID,如果有分 section 就也要算上它的
// 自定義一個DetailButton類,繼承自UIButton
class DetailButton: UIButton {
// 添加一個sectionID屬性,用來存儲與按鈕相關的PokemonItem的ID
// 這使得在按鈕的事件處理中,能夠確定按鈕屬於哪一個section
var sectionID: PokemonItem.ID?
// 添加一個rowID屬性,用來存儲與按鈕相關的Item的ID
// 這樣在處理按鈕事件時,可以知道是哪一個具體的Item觸發了事件
var rowID: Item.ID?
}
cell 上的按鈕的類別就是自製的
@IBOutlet weak var detailButton: DetailButton!
在設置資料給 cell 時,也將每個資料的 ID 傳給這個按鈕
兩個長得不太一樣因為下面的那行是原本就寫在設置 cell 的方法裡的,上面那行是後來發現 section 自己也要有 ID 而補上去的,但簡言之就是按鈕必須要收到這兩個 ID 就對了
ballCell.detailButton.sectionID = items[indexPath.section].id
detailButton.rowID = item.id
新增一個 swift 檔,擴展陣列自製一個方法,我沒有那麼聰明這個就是完全照抄彼得那篇文章的 XD,但這個確實有種讓人大開眼界覺得程式碼有被好好運用到的感覺
import UIKit
// 擴展 Array 類型,但這個擴展僅適用於其元素遵循 Identifiable 協議的情況
// Identifiable 是 Swift 標準庫中的一個協議,要求實現它的類型提供一個唯一標識符(id)
extension Array where Element: Identifiable {
// 定義一個方法,接受一個 Identifiable 協議中定義的 ID 類型的參數
// 此方法的目的是找到數組中具有給定 ID 的元素的索引
func indexOfElement(with id: Element.ID) -> Self.Index {
// 使用 firstIndex(where:) 方法來尋找第一個 ID 匹配給定ID的元素的索引
// $0.id == id 這部分是一個閉包,用於比較每個元素的 id 與傳入的 id 是否相同
guard let index = firstIndex(where: { $0.id == id }) else {
// 如果找不到匹配的元素,則使用 fatalError() 拋出一個運行時錯誤
// 在實際開發中,拋出錯誤可能不是最好的選擇,你可能會選擇返回nil或進行其他處理
fatalError()
}
return index
}
}
再來的做法我是選用 delegate 來傳資料,但運用 delegate 我自己也還是有點一知半解就是
先在 cell 實現 delegate,並設置遵從的屬性
// 定義一個協議,用於當單元格中的詳情按鈕被點擊時通知代理執行相應的動作
protocol categoryBallTableViewCellDelegate: AnyObject {
func detailButtonTap(sender: DetailButton)
}
class categoryBallTableViewCell: UITableViewCell {
weak var delegate: AppTableViewCellDelegate?
@IBAction func detailButtonTap(_ sender: DetailButton) {
// 通知代理執行detailButtonTap方法
delegate?.detailButtonTap(sender: sender)
}
在本頁的 controller 遵從這個 protocol 並將代理設為自己,最後定義方法
ballCell.delegate = self
// 擴展PokeItemTableViewController以實現categoryBallTableViewCellDelegate協議
extension PokeItemTableViewController: categoryBallTableViewCellDelegate {
// 當categoryBallTableViewCell中的詳情按鈕被點擊時調用
func detailButtonTap(sender: DetailButton) {
// 嘗試從按鈕獲取所在的區段ID
guard let sectionID = sender.sectionID else { return }
// 使用區段ID找到對應的區段索引
let section = items.indexOfElement(with: sectionID)
// 嘗試從按鈕獲取所在的行ID
guard let rowID = sender.rowID else { return }
// 使用行ID找到對應的行索引
let row = items[section].item.indexOfElement(with: rowID)
// 根據區段索引和行索引獲取選中的物品
let selectItem = items[section].item[row]
// 輸出選中物品的名稱,這行主要用於調試
print(selectItem.name)
// 從Storyboard實例化詳情視圖控制器
let controller = storyboard?.instantiateViewController(withIdentifier: PokeItemDetailViewController.reuseIdentifier) as! PokeItemDetailViewController
// 設置詳情視圖控制器的物品
controller.item = selectItem
// 以模態方式呈現詳情視圖控制器
present(controller, animated: true)
// 或者將詳情視圖控制器推送到導航控制器上,根據具體需求選擇使用
// navigationController?.pushViewController(controller, animated: true)
}
}
以上,程式碼的部分都只有貼上符合該說明項目的所以可能看起來怪怪的 XD