iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
生成式 AI

iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線系列 第 12

Day 12 - 【鬧鐘實戰 III】畫面施工:用 UITableView 打造列表 UI

  • 分享至 

  • xImage
  •  

昨天,我們用 UITabBarController 把 App 的幾個主要功能區都隔出來了,感覺 App 好像有模有樣了。但點到「鬧鐘」分頁,眼前還是一片空白,老實說,我有點不知道該從何下手。

我的目標很明確:要在這個畫面上顯示一個鬧鐘列表。如果要用列表就是 UITableView

操作步驟:

  1. 打開 MainViewController.xib 檔案。
  2. 點擊 Xcode 右上角的 + 按鈕,打開物件庫 (Library)。
  3. 在搜尋框中輸入 Table View
  4. Table View 物件拖曳到畫布上。
  5. 設定 Auto Layout 約束: 我們要讓 Table View 永遠填滿整個安全區域。
    • 點選剛剛拖入的 Table View

    • 點擊編輯區右下角中間的「新增約束」(領帶結) 按鈕。

      image

    • 將上、下、左、右 (Top, Bottom, Leading, Trailing) 四個邊距都設為 0,並點亮旁邊的四條紅色實線。

    • 點擊 Add 4 Constraints

  6. 連結 IBOutlet:
    • 打開助理編輯器 (Assistant Editor)⬇️,確保右邊顯示的是 MainViewController.swift

      image

    • 按住 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 協作

    • 我的 Prompt:我正在用 UITableView 做一個鬧鐘列表,但我卡住了。我的列表有兩種長得完全不一樣的 Cell,而且其中一種 Cell 裡面還塞了兩個 UILabel 和一個 UISwitch。我該怎麼辦?請簡單說明

    AI 的回答:

    如果你正在使用 UITableView 建立一個鬧鐘列表,且其中有兩種樣式完全不同的 Cell,這是一個很常見的設計情境。要處理這個問題,核心概念是在產生每個 Cell 時,先判斷它應該屬於哪一種樣式,然後從 UITableViewCell 重複使用池(Reusable Cell Pool) 中取出正確的 Cell 來使用。

    以下是處理這個問題的簡單步驟說明:


    1. 區分你的資料類型

    首先,你需要一個方法來區分你的資料來源(例如,一個儲存鬧鐘設定的陣列)中的每一個項目。你可以為每個資料項目設定一個屬性,例如 cellType,或者使用 Enum 來定義不同的類型,例如 case alarmCellcase infoCell

    這樣當你在處理第 N 個 Cell 時,就可以根據這個類型來決定要顯示哪一種 Cell。


    2. 為每種 Cell 註冊唯一的識別碼

    在你的 ViewController 載入時(通常是 viewDidLoad),你需要為兩種不同樣式的 Cell 各自註冊一個唯一的識別碼(Identifier)

    • 例如,給鬧鐘 Cell 註冊一個 "AlarmCell" 識別碼。
    • 給另一種 Cell 註冊一個 "InfoCell" 識別碼。

    這個識別碼的作用就像是 Cell 的身分證字號,UITableView 會根據這個號碼來取用或建立對應的 Cell。


    3. 在 cellForRowAt 方法中進行判斷

    這是最關鍵的一步。在 tableView(_:cellForRowAt:) 這個方法中,你需要這樣做:

    1. 根據當前 indexPath 取得對應的資料項目。
    2. 判斷這個資料項目的類型:它是屬於鬧鐘資料,還是另一種 Cell 的資料?
    3. 根據判斷結果,使用 tableView.dequeueReusableCell(withIdentifier:) 方法,並傳入對應的識別碼 ("AlarmCell""InfoCell"),從重複使用池中取出正確的 Cell。
    4. 最後,針對取出的 Cell 進行內容配置,例如設定 UILabel 的文字或 UISwitch 的開關狀態。

    總結來說,就是「在產生 Cell 時,先問資料它是誰,再拿出對應的 Cell 來顯示」。這種模式讓你可以在同一個 UITableView 中,靈活地展示多種不同介面佈局的 Cell。

    • 根據 AI 的建議,我把詳細的實作步驟整理如下:
      1. 設計藍圖 (Custom Cells):建立 AlarmTableViewCellSleepAlarmTableViewCell 兩個自訂 Cell 的 .swift.xib 檔案,並在 XIB 中完成 UI 佈局與連結。
      2. 整合主角:將 TabBarController 中的佔位 UIViewController 換成我們真正的 MainViewController
      3. 組裝列表:在 MainViewController 中註冊我們設計好的 Cell,並實作 DataSource 協議,用假資料讓列表先顯示出來。

步驟1:設計自訂的 AlarmTableViewCell & SleepAlarmTableViewCell

操作步驟:

  1. 在專案中建立一個 View 資料夾,並在其中再建立一個 Cells 子資料夾。

  2. Cells 資料夾上右鍵,選擇 New File... -> Cocoa Touch Class

  3. 進行如下設定:

    • Class: 輸入 AlarmTableViewCell
    • Subclass of: 選擇 UITableViewCell
    • Also create XIB file: 務必勾選
  4. 點擊 Next 並建立檔案。

  5. 打開 AlarmTableViewCell.xib 檔案。您會看到一個空白的 Cell 畫布,這是我們接下來要施工的地方。

  6. 放置元件: 從物件庫 (+) 中,拖曳以下元件到畫布上:

    • 2 個 UILabel:一個用來顯示大的時間數字,另一個顯示小的鬧鐘名稱。
    • 1 個 UISwitch:用來控制鬧鐘的開關。
    • 1 個 UIStackView:這是一個強大的佈局工具,我們將用它來管理左邊那兩個上下排列的 Label。
  7. 約束設定

    現在,我們來一步步地為這些元件設定 Auto Layout 約束。我們的目標是:讓開關永遠在右邊,而兩個 Label 在左邊上下排列,並填滿剩餘空間。

    第 1 步:定位右側的開關 (UISwitch)

    我們先把最容易定位的開關「釘」在它應該在的位置。

    1. 點選畫布上的 UISwitch。

    2. 點擊右下角的「新增約束」(領帶結) 按鈕。在彈出的面板中,設定右側 (Trailing) 邊距為 16。

    3. 接著,點擊「對齊約束」(領帶結左邊的按鈕)。在彈出的面板中,勾選 Vertically in Container

      為什麼? 這兩步告訴 Xcode:「這個開關,請永遠保持在距離 Cell 右邊緣 16 點的位置,並且垂直居中。」

    第 2 步:用 Stack View 組合左側的標籤

    對於上下排列的元件,使用「垂直 Stack View」來管理是最高效的方法。

    1. 從物件庫 (+) 中,拖曳一個 Vertical Stack View 到畫布的左側。
    2. 將我們之前拖進來的兩個 UILabel,都拖曳到這個 Stack View 裡面。在左側的文件大綱中操作會更精準。
    3. 點選這個 UIStackView。在右側的「屬性檢查器」中,進行如下設定:
      • Alignment: Fill
      • Distribution: Fill Equally (讓兩個 Label 平分 Stack View 的高度)
      • Spacing: 4 (設定兩個 Label 之間的垂直間距)

    第 3 步:定位左側的 Stack View

    現在,我們只需要為這個裝著兩個 Label 的「容器」(Stack View) 設定約束即可。

    1. 點選 UIStackView。

    2. 點擊「新增約束」按鈕。

    3. 設定以下四個約束:

      • 左側 (Leading): 設為 16,與父視圖 (Cell) 的左邊緣對齊。
      • 頂部 (Top): 設為 10 (留出一些上下邊距),與父視圖的頂部對齊。
      • 底部 (Bottom): 設為 10,與父視圖的底部對齊。
      • 右側 (Trailing): 設為 8 (留出一些間距),但這次的對齊目標要選擇右邊的 UISwitch

      為什麼? 這四步完整地定義了 Stack View 的位置和大小。它的上下左右都被「釘」住了:左邊貼著 Cell,右邊貼著開關,上下也貼著 Cell。這樣,它就會自動被拉伸,填滿左側所有可用的空間。

      image

    第 4 步:連結 IBOutlet

    最後,將這些設定好的 UI 元件與程式碼連結起來。

    1. 打開助理編輯器,左邊是 .xib,右邊是 AlarmTableViewCell.swift。
    2. 上面大的 Label 拖線到程式碼,命名為 lbTime。
    3. 下面小的 Label 拖線到程式碼,命名為 lbName。
    4. 從 UISwitch 拖線到程式碼,命名為 swAlarm。
    5. 別忘了在 AlarmTablexaViewCell.swift 中加入靜態的 identifier。**static** **let** identifier = "AlarmTableViewCell"
  8. 建立 SleepAlarmTableViewCell (靜態外觀):

    • 在 View/Cells 資料夾中,建立一個名為 SleepAlarmTableViewCell 的 Cocoa Touch Class,記得勾選 Also create XIB file
    • 設計 UI: 打開 .xib,拖入 UIImageView, UILabel, UIButton 等元件,並設定好約束,使其外觀與原生時鐘 App 的「睡眠」區塊一致。

    image

    • 加入 identifier: 在 SleepAlarmTableViewCell.swift 中加入 static let identifier = "SleepAlarmTableViewCell"。

步驟2:實現分區靜態列表

現在,我們回到 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,讓它遵守 UITableViewDataSourceUITableViewDelegate 協議,並實作顯示分區列表的方法。

// 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 + RApp 運行起來了,切到鬧鐘頁籤,也真的出現了!

明天,我們將真正為這個畫面注入靈魂:連接上 Realm 資料庫,讓列表顯示真實的鬧鐘數據,並實作最核心的「左滑刪除」功能!


上一篇
Day 11 - 【鬧鐘實戰 II】App 框架搭建:用 TabBarController 打造我們的 App 骨架
系列文
iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
yulin494
iT邦新手 4 級 ‧ 2025-09-26 19:09:56

期待明天注入靈魂的Update!還有 realm 資料庫的運用!/images/emoticon/emoticon08.gif

敬請期待,明天就要把假資料換掉了!/images/emoticon/emoticon08.gif

我要留言

立即登入留言