S SmartDocs
Série: iOS App swift 248 linhas · Atualizado 2026-07-15

ViewController.swift

iOS_App/TodoList-Storyboard/TodoList-Storyboard/ViewController.swift

//
//  ViewController.swift
//  TodoList-Storyboard
//
//  Created by Nelson Lai on 2026/7/15.
//

import UIKit
import CoreData

class ViewController: UIViewController {

    // MARK: - IBOutlet

    /// 與 Storyboard 中的 Table View 連結的 Outlet
    @IBOutlet weak var tableView: UITableView!

    // MARK: - 屬性

    /// 存放所有待辦事項的陣列(資料來源)
    var todoItems: [TodoItem] = []

    /// Core Data 的管理物件內容(Managed Object Context),
    /// 透過 AppDelegate 的 persistentContainer 取得
    var context: NSManagedObjectContext {
        (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    }

    /// 當清單為空時顯示的提示標籤
    let emptyStateLabel: UILabel = {
        let label = UILabel()
        label.text = "目前沒有待辦事項\n點右上角「+」新增一筆吧!"
        label.numberOfLines = 0
        label.textAlignment = .center
        label.textColor = .secondaryLabel
        label.font = .preferredFont(forTextStyle: .body)
        return label
    }()

    /// 用來把 Date 轉成易讀字串的日期格式器
    let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        formatter.locale = Locale(identifier: "zh_Hant_TW")
        return formatter
    }()

    // MARK: - 生命週期

    override func viewDidLoad() {
        super.viewDidLoad()

        // 讓導覽列顯示大標題
        navigationController?.navigationBar.prefersLargeTitles = true

        // 設定空狀態提示為 Table View 的背景視圖
        tableView.backgroundView = emptyStateLabel

        // 從 Core Data 讀取已儲存的待辦事項
        fetchTodoItems()
    }

    // MARK: - Core Data 操作

    /// 從 Core Data 讀取所有待辦事項,並依建立時間排序(新的在前)
    func fetchTodoItems() {
        let request: NSFetchRequest<TodoItem> = TodoItem.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]

        do {
            todoItems = try context.fetch(request)
        } catch {
            print("讀取資料失敗:\(error)")
        }

        // 依資料是否為空,決定要不要顯示空狀態提示
        emptyStateLabel.isHidden = !todoItems.isEmpty
        tableView.reloadData()
    }

    /// 將 context 中的變更寫入永久儲存區,並重新載入畫面
    func saveAndReload() {
        do {
            try context.save()
        } catch {
            print("儲存資料失敗:\(error)")
        }
        fetchTodoItems()
    }

    // MARK: - IBAction

    /// 使用者點擊右上角「+」按鈕時呼叫,跳出輸入視窗新增待辦事項
    @IBAction func addButtonTapped(_ sender: UIBarButtonItem) {
        let alert = UIAlertController(title: "新增待辦事項",
                                      message: "請輸入事項內容",
                                      preferredStyle: .alert)

        // 在 Alert 中加入一個文字輸入框
        alert.addTextField { textField in
            textField.placeholder = "例如:買牛奶"
        }

        // 「新增」按鈕:取得輸入文字並建立新的 TodoItem
        let addAction = UIAlertAction(title: "新增", style: .default) { [weak self] _ in
            guard let self = self,
                  let text = alert.textFields?.first?.text?
                      .trimmingCharacters(in: .whitespacesAndNewlines),
                  !text.isEmpty else { return }

            // 在 Core Data 的 context 中建立一筆新資料
            let newItem = TodoItem(context: self.context)
            newItem.title = text
            newItem.isCompleted = false
            newItem.createdAt = Date()

            self.saveAndReload()
        }

        // 「取消」按鈕:不做任何事
        let cancelAction = UIAlertAction(title: "取消", style: .cancel)

        alert.addAction(addAction)
        alert.addAction(cancelAction)
        present(alert, animated: true)
    }

    // MARK: - 編輯待辦事項

    /// 跳出輸入視窗,讓使用者修改既有事項的標題
    func presentEditAlert(for item: TodoItem) {
        let alert = UIAlertController(title: "編輯待辦事項",
                                      message: "請修改事項內容",
                                      preferredStyle: .alert)

        alert.addTextField { textField in
            textField.text = item.title
        }

        let saveAction = UIAlertAction(title: "儲存", style: .default) { [weak self] _ in
            guard let self = self,
                  let text = alert.textFields?.first?.text?
                      .trimmingCharacters(in: .whitespacesAndNewlines),
                  !text.isEmpty else { return }

            item.title = text
            self.saveAndReload()
        }

        alert.addAction(saveAction)
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        present(alert, animated: true)
    }
}

// MARK: - UITableViewDataSource

extension ViewController: UITableViewDataSource {

    /// 回傳列表的列數 = 待辦事項的數量
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return todoItems.count
    }

    /// 設定每一列儲存格的內容
    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // 從重用佇列取出識別碼為 "TodoCell" 的儲存格
        let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell",
                                                 for: indexPath)
        let item = todoItems[indexPath.row]

        // 已完成的事項:加上刪除線並變灰
        let title = item.title ?? ""
        if item.isCompleted {
            let attributed = NSAttributedString(
                string: title,
                attributes: [
                    .strikethroughStyle: NSUnderlineStyle.single.rawValue,
                    .foregroundColor: UIColor.secondaryLabel
                ])
            cell.textLabel?.attributedText = attributed
        } else {
            cell.textLabel?.attributedText = nil
            cell.textLabel?.text = title
            cell.textLabel?.textColor = .label
        }

        // 副標題顯示建立時間
        if let createdAt = item.createdAt {
            cell.detailTextLabel?.text = dateFormatter.string(from: createdAt)
        } else {
            cell.detailTextLabel?.text = nil
        }
        cell.detailTextLabel?.textColor = .secondaryLabel

        // 已完成的事項在右側顯示勾勾
        cell.accessoryType = item.isCompleted ? .checkmark : .none

        return cell
    }
}

// MARK: - UITableViewDelegate

extension ViewController: UITableViewDelegate {

    /// 點擊某一列:切換「已完成 / 未完成」狀態
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        let item = todoItems[indexPath.row]
        item.isCompleted.toggle()
        saveAndReload()
    }

    /// 向左滑動:顯示「刪除」按鈕
    func tableView(_ tableView: UITableView,
                   trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
    -> UISwipeActionsConfiguration? {
        let deleteAction = UIContextualAction(style: .destructive,
                                              title: "刪除") { [weak self] _, _, completion in
            guard let self = self else { return }

            // 從 Core Data 中刪除該筆資料
            let item = self.todoItems[indexPath.row]
            self.context.delete(item)
            self.saveAndReload()
            completion(true)
        }
        return UISwipeActionsConfiguration(actions: [deleteAction])
    }

    /// 向右滑動:顯示「編輯」按鈕
    func tableView(_ tableView: UITableView,
                   leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
    -> UISwipeActionsConfiguration? {
        let editAction = UIContextualAction(style: .normal,
                                            title: "編輯") { [weak self] _, _, completion in
            guard let self = self else { return }
            self.presentEditAlert(for: self.todoItems[indexPath.row])
            completion(true)
        }
        editAction.backgroundColor = .systemBlue
        return UISwipeActionsConfiguration(actions: [editAction])
    }
}

Artigos relacionados