You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS阅读进度追踪App:TableView Cell属性更新失败求助

书籍阅读进度追踪App同步更新问题解决方案

问题背景

我正在开发一款书籍阅读进度追踪App,核心模块功能:

  • HomeViewController:包含TableView,展示已添加的书籍,每行显示书籍标题、进度条及当前页码/总页码
  • AddBookController:负责添加书籍数据,通过代理将数据传递给HomeViewController,生成TableView行
  • BookDetailViewController:点击TableView任意行进入该页面,用于修改书籍的当前阅读页码

遇到的问题:在BookDetailViewController中更新页码后,TableView对应Cell的currentPage无法同步更新。虽然能把更新后的页码传递到HomeViewController,但不知道如何让Cell的数值与新值对齐,尝试过didSet观察者但未成功。

相关代码

HomeViewController

class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SendingBookDataProtocol {
    
    // 无用变量,之前尝试的didSet未生效
    // var updatedPage = String() {
    //     didSet {
    //         print(updatedPage)
    //         tableView?.reloadData()
    //     }
    // }
    
    var items = [BookItem]()
    var item: BookItem?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView?.delegate = self
        tableView?.dataSource = self
        
        let nib = UINib(nibName: "BookCell", bundle: nil)
        tableView?.register(nib, forCellReuseIdentifier: "BookCell")
    }
    
    func sendDataToHomeController(bookEntry item: BookItem) {
        items.append(item)
        tableView.reloadData()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let bookDetailVc = self.storyboard?.instantiateViewController(withIdentifier: "BookDetailView") as? BookDetailViewController
        let item = items[indexPath.row]
        
        let currentPageInt = Float(item.currentPage)!
        let totalPagesInt = Float(item.totalPages)!
        let result = Int(totalPagesInt - currentPageInt)
        let percentageRead = Int((currentPageInt / totalPagesInt) * 100)
        
        bookDetailVc?.lblName = item.title
        bookDetailVc?.lblCurrentPage = Int(Float(item.currentPage)!)
        // ...其他传递数据的代码
        
        self.navigationController!.pushViewController(bookDetailVc!, animated: true)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell", for: indexPath) as! BookCell
        item = items[indexPath.row]
        
        cell.title.text = item?.title
        // ...其他Cell赋值代码
        cell.pageNumbers.text = "S. " + item!.currentPage + " / " + item!.totalPages // 需要更新currentPage
        return cell
    }
}

BookDetailViewController

class BookDetailViewController: HomeViewController, UIPickerViewDelegate, UIPickerViewDataSource{

    @IBOutlet weak var bookTitle: UILabel!
    @IBOutlet weak var numberPicker: UIPickerView!
    
    var lblName = String()
    var lblCurrentPage = Int()
    var pickerData: [String] = [] // 假设已初始化页码数据

    override func viewDidLoad() {
        super.viewDidLoad()

        self.numberPicker.delegate = self
        self.numberPicker.dataSource = self
        // ...其他初始化代码
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let valueSelected = pickerData[row] as String
        
        // 错误:实例化了新的HomeViewController,而非当前导航栈中的实例
        if let homeVc = storyboard?.instantiateViewController(withIdentifier: "getBookData") as? HomeViewController{
            homeVc.updatedPage = valueSelected
            homeVc.tableView?.reloadData()
        }
    }
    
    // 补充Picker数据源方法
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return pickerData.count
    }
}

AddBookController

protocol SendingBookDataProtocol {
    func sendDataToHomeController(bookEntry: BookItem)
}

struct BookItem {
    let title,author,currentPage,totalPages:String
    let image: UIImage?
}

class AddBookController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    var delegate: SendingBookDataProtocol? = nil

    @IBAction func buttonSave(_ sender: Any) {
        let bookEntry = BookItem(title: textfieldTitle.text!, author: textfieldAuthor.text!, currentPage: fieldCurrentPage.text!, totalPages: fieldTotalPages.text!, image: bookImage)
        
        self.delegate?.sendDataToHomeController(bookEntry: bookEntry)
        dismiss(animated: true, completion: nil)
    }
}

问题根源

  1. 数据模型不可修改:BookItem的currentPage用let声明,是不可变属性,无法修改值
  2. 错误的页面实例引用:在BookDetailViewController中通过storyboard实例化了新的HomeViewController,而非导航栈中已存在的实例,导致更新的是新对象的变量,对原页面无影响
  3. 未更新数据源:即使传递了新页码,也没有修改items数组中对应BookItem的currentPage值,reloadData后仍显示旧数据

解决方案

步骤1:修改数据模型,允许修改currentPage

将BookItem的currentPage从let改为var:

struct BookItem {
    let title, author, totalPages: String
    var currentPage: String // 改为可变属性
    let image: UIImage?
}

步骤2:给BookDetailViewController添加回调闭包

用闭包代替错误的页面实例化,实现数据回传:

class BookDetailViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource{

    @IBOutlet weak var bookTitle: UILabel!
    @IBOutlet weak var numberPicker: UIPickerView!
    
    // 替换原有的lblName、lblCurrentPage,用更清晰的命名
    var bookTitleText: String = ""
    var currentPage: Int = 0
    var totalPages: Int = 0
    // 回调闭包,用于传递更新后的页码
    var updatePageHandler: ((Int) -> Void)?
    var pickerData: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        
        numberPicker.delegate = self
        numberPicker.dataSource = self
        
        // 初始化页面数据
        bookTitle.text = bookTitleText
        // 生成Picker的页码数据(从1到总页数)
        pickerData = (1...totalPages).map { String($0) }
        // 默认选中当前页码
        if let selectedIndex = pickerData.firstIndex(of: String(currentPage)) {
            numberPicker.selectRow(selectedIndex, inComponent: 0, animated: false)
        }
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let selectedPage = Int(pickerData[row]) ?? currentPage
        // 调用闭包传递更新后的页码
        updatePageHandler?(selectedPage)
    }
    
    // Picker数据源方法实现
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return pickerData.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return pickerData[row]
    }
}

步骤3:修改HomeViewController的跳转逻辑,传递索引和回调

在跳转到BookDetailViewController时,传递必要数据并设置回调闭包,更新数据源并刷新对应Cell:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    guard let bookDetailVc = storyboard?.instantiateViewController(withIdentifier: "BookDetailView") as? BookDetailViewController else { return }
    
    let item = items[indexPath.row]
    // 传递页面所需数据
    bookDetailVc.bookTitleText = item.title
    bookDetailVc.currentPage = Int(item.currentPage) ?? 1
    bookDetailVc.totalPages = Int(item.totalPages) ?? 1
    
    // 设置回调闭包,更新数据源并刷新当前行
    bookDetailVc.updatePageHandler = { [weak self] newPage in
        guard let self = self else { return }
        // 修改数据源中对应书籍的currentPage
        self.items[indexPath.row].currentPage = String(newPage)
        // 只刷新当前行,性能更优
        self.tableView.reloadRows(at: [indexPath], with: .automatic)
    }
    
    navigationController?.pushViewController(bookDetailVc, animated: true)
}

步骤4:移除无用代码

删除HomeViewController中无用的updatedPage变量及didSet观察者,因为已经通过闭包实现了正确的数据更新逻辑。

效果验证

修改完成后,在BookDetailViewController中选择新的页码,返回HomeViewController时,对应Cell的当前页码会自动更新,且只刷新修改的行,提升性能。

内容的提问来源于stack exchange,提问作者alias.marv

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 14:50:34