昨天,我們用 UITabBarController
把 App 的幾個主要功能區都隔出來了,感覺 App 好像有模有樣了。但點到「鬧鐘」分頁,眼前還是一片空白,老實說,我有點不知道該從何下手。
我的目標很明確:要在這個畫面上顯示一個鬧鐘列表。如果要用列表就是 UITableView
。
操作步驟:
MainViewController.xib
檔案。+
按鈕,打開物件庫 (Library)。Table View
。Table View
物件拖曳到畫布上。Table View
永遠填滿整個安全區域。
點選剛剛拖入的 Table View
。
點擊編輯區右下角中間的「新增約束」(領帶結) 按鈕。
將上、下、左、右 (Top, Bottom, Leading, Trailing) 四個邊距都設為 0
,並點亮旁邊的四條紅色實線。
點擊 Add 4 Constraints
。
IBOutlet
:
打開助理編輯器 (Assistant Editor)⬇️,確保右邊顯示的是 MainViewController.swift
。
按住 Control
鍵,從 .xib
畫布上的 Table View
拖一條線到程式碼中,將其命名為 tbvAlarm
。
MainViewController.swift
(新增 IBOutlet)
import UIKit
class MainViewController: UIViewController {
@IBOutlet weak var tbvAlarm: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// 我們之後會在這裡設定 TableView
}
}
困惑點:我想顯示的每一行,左邊要有大大的時間、下面有小小的標籤,右邊還要有一個開關呢?這看起來比陽春的單行文字列表複雜多了。而且 App 頂部還有一個長相完全不一樣的「睡眠」區塊。
AI 協作
我正在用 UITableView 做一個鬧鐘列表,但我卡住了。我的列表有兩種長得完全不一樣的 Cell,而且其中一種 Cell 裡面還塞了兩個 UILabel 和一個 UISwitch。我該怎麼辦?請簡單說明
AI 的回答:
如果你正在使用
UITableView
建立一個鬧鐘列表,且其中有兩種樣式完全不同的 Cell,這是一個很常見的設計情境。要處理這個問題,核心概念是在產生每個 Cell 時,先判斷它應該屬於哪一種樣式,然後從UITableView
的 Cell 重複使用池(Reusable Cell Pool) 中取出正確的 Cell 來使用。以下是處理這個問題的簡單步驟說明:
1. 區分你的資料類型
首先,你需要一個方法來區分你的資料來源(例如,一個儲存鬧鐘設定的陣列)中的每一個項目。你可以為每個資料項目設定一個屬性,例如
cellType
,或者使用Enum
來定義不同的類型,例如case alarmCell
和case infoCell
。這樣當你在處理第 N 個 Cell 時,就可以根據這個類型來決定要顯示哪一種 Cell。
2. 為每種 Cell 註冊唯一的識別碼
在你的
ViewController
載入時(通常是viewDidLoad
),你需要為兩種不同樣式的 Cell 各自註冊一個唯一的識別碼(Identifier)。
- 例如,給鬧鐘 Cell 註冊一個
"AlarmCell"
識別碼。- 給另一種 Cell 註冊一個
"InfoCell"
識別碼。這個識別碼的作用就像是 Cell 的身分證字號,
UITableView
會根據這個號碼來取用或建立對應的 Cell。
3. 在
cellForRowAt
方法中進行判斷這是最關鍵的一步。在
tableView(_:cellForRowAt:)
這個方法中,你需要這樣做:
- 根據當前
indexPath
取得對應的資料項目。- 判斷這個資料項目的類型:它是屬於鬧鐘資料,還是另一種 Cell 的資料?
- 根據判斷結果,使用
tableView.dequeueReusableCell(withIdentifier:)
方法,並傳入對應的識別碼 ("AlarmCell"
或"InfoCell"
),從重複使用池中取出正確的 Cell。- 最後,針對取出的 Cell 進行內容配置,例如設定
UILabel
的文字或UISwitch
的開關狀態。總結來說,就是「在產生 Cell 時,先問資料它是誰,再拿出對應的 Cell 來顯示」。這種模式讓你可以在同一個
UITableView
中,靈活地展示多種不同介面佈局的 Cell。
AlarmTableViewCell
和 SleepAlarmTableViewCell
兩個自訂 Cell 的 .swift
與 .xib
檔案,並在 XIB 中完成 UI 佈局與連結。TabBarController
中的佔位 UIViewController
換成我們真正的 MainViewController
。MainViewController
中註冊我們設計好的 Cell,並實作 DataSource
協議,用假資料讓列表先顯示出來。操作步驟:
在專案中建立一個 View
資料夾,並在其中再建立一個 Cells
子資料夾。
在 Cells
資料夾上右鍵,選擇 New File...
-> Cocoa Touch Class
。
進行如下設定:
AlarmTableViewCell
。UITableViewCell
。點擊 Next
並建立檔案。
打開 AlarmTableViewCell.xib
檔案。您會看到一個空白的 Cell 畫布,這是我們接下來要施工的地方。
放置元件: 從物件庫 (+) 中,拖曳以下元件到畫布上:
約束設定
現在,我們來一步步地為這些元件設定 Auto Layout 約束。我們的目標是:讓開關永遠在右邊,而兩個 Label 在左邊上下排列,並填滿剩餘空間。
第 1 步:定位右側的開關 (UISwitch)
我們先把最容易定位的開關「釘」在它應該在的位置。
點選畫布上的 UISwitch。
點擊右下角的「新增約束」(領帶結) 按鈕。在彈出的面板中,設定右側 (Trailing) 邊距為 16。
接著,點擊「對齊約束」(領帶結左邊的按鈕)。在彈出的面板中,勾選 Vertically in Container。
為什麼? 這兩步告訴 Xcode:「這個開關,請永遠保持在距離 Cell 右邊緣 16 點的位置,並且垂直居中。」
第 2 步:用 Stack View 組合左側的標籤
對於上下排列的元件,使用「垂直 Stack View」來管理是最高效的方法。
第 3 步:定位左側的 Stack View
現在,我們只需要為這個裝著兩個 Label 的「容器」(Stack View) 設定約束即可。
點選 UIStackView。
點擊「新增約束」按鈕。
設定以下四個約束:
為什麼? 這四步完整地定義了 Stack View 的位置和大小。它的上下左右都被「釘」住了:左邊貼著 Cell,右邊貼著開關,上下也貼著 Cell。這樣,它就會自動被拉伸,填滿左側所有可用的空間。
第 4 步:連結 IBOutlet
最後,將這些設定好的 UI 元件與程式碼連結起來。
static** **let** identifier = "AlarmTableViewCell"
建立 SleepAlarmTableViewCell (靜態外觀):
static let identifier = "SleepAlarmTableViewCell"。
現在,我們回到 MainViewController.swift
,把剛剛做好的兩個 Cell 組裝起來。
第 1 步:註冊自定義 Cell
在 MainViewController.swift
中,建立一個 setupTableView()
方法,並在 viewDidLoad
中呼叫它。這個方法會告訴 TableView 我們有兩個新的自定義 Cell。
// In MainViewController.swift
import UIKit
import RealmSwift
class MainViewController: UIViewController {
// MARK: - IBOutlet
@IBOutlet weak var tbvAlarm: UITableView!
// MARK: - Property
var alarms: Results<AlarmData>!
/// Realm 資料庫實例
var realm: Realm!
/// 記錄是否處於編輯模式
var isEditingMode: Bool = false
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
// MARK: - UI Setting
// MARK: - IBAcion
// MARK: - Function
private func setupTableView() {
tbvAlarm.dataSource = self
tbvAlarm.delegate = self
// 註冊我們自訂的 XIB Cell
tbvAlarm.register(
UINib(nibName: AlarmTableViewCell.identifier, bundle: nil),
forCellReuseIdentifier: AlarmTableViewCell.identifier
)
tbvAlarm.register(
UINib(nibName: SleepAlarmTableViewCell.identifier, bundle: nil),
forCellReuseIdentifier: SleepAlarmTableViewCell.identifier
)
tbvAlarm.rowHeight = 100
}
}
知識點:register 的作用是告訴 TableView:「當程式碼向你索取一個 identifier 為 "AlarmTableViewCell" 的 Cell 時,請你去 AlarmTableViewCell.xib 這個檔案裡幫我實例化一個出來。」
第 2 步:實現分區邏輯 (DataSource
)
在 MainViewController.swift
檔案的底部,加入 extension
,讓它遵守 UITableViewDataSource
和 UITableViewDelegate
協議,並實作顯示分區列表的方法。
// MARK: - TableView 資料源和代理
extension MainViewController: UITableViewDataSource, UITableViewDelegate {
// 告訴 TableView,我們有 2 個分區 (Section)
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
// 告訴 TableView,每個分區有幾行
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1 // 第一個分區 (睡眠) 只有 1 行
} else {
return 3 // 第二個分區 (其他鬧鐘) 暫時顯示 3 行假資料
}
}
// 設置行高
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 {
return 100 //
} else {
return 100 // 鬧鐘 cell 的高度
}
}
/// 設定每個區段標題的高度
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 1 && (alarms?.count ?? 0) > 0 {
return 40
}
return 0
}
/// 提供每個區段的自訂標題視圖 (View)
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard section == 1, (alarms?.count ?? 0) > 0 else { return nil }
let headerLabel = UILabel()
headerLabel.text = "其他"
headerLabel.textColor = .white
headerLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) // 在這裡設定了放大加粗
headerLabel.frame = CGRect(x: 16, y: 0, width: tableView.frame.width - 32, height: 40)
let headerView = UIView()
headerView.addSubview(headerLabel)
return headerView
}
// 設定每一行的內容
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 如果是第一個分區
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(
withIdentifier: SleepAlarmTableViewCell.identifier,
for: indexPath
) as! SleepAlarmTableViewCell
// ... 未來可以設定 cell 內容 ...
return cell
// 如果是第二個分區
} else {
let cell = tableView.dequeueReusableCell(
withIdentifier: AlarmTableViewCell.identifier,
for: indexPath
) as! AlarmTableViewCell
// 填入假資料
cell.lbTime.text = "0\(indexPath.row + 6):00" // 06:00, 07:00, 08:00
cell.lbName.text = "假資料, 每天"
cell.swAlarm.isOn = (indexPath.row % 2 == 0) // 交錯開關
return cell
}
}
}
現在按下 Cmd + R
App 運行起來了,切到鬧鐘頁籤,也真的出現了!
明天,我們將真正為這個畫面注入靈魂:連接上 Realm 資料庫,讓列表顯示真實的鬧鐘數據,並實作最核心的「左滑刪除」功能!