【问题标题】:In a storyboard, how do I make a custom cell for use with multiple controllers?在情节提要中,如何制作自定义单元格以与多个控制器一起使用?
【发布时间】:2012-03-03 23:54:10
【问题描述】:

我正在尝试在我正在开发的应用中使用故事板。在应用程序中有 Lists 和 Users 并且每个都包含其他的集合(列表的成员,用户拥有的列表)。因此,因此,我有 ListCell 和 UserCell 课程。目标是让它们在整个应用程序中可重用(即,在我的任何 tableview 控制器中)。

这就是我遇到问题的地方。

如何在情节提要中创建可在任何视图控制器中重复使用的自定义 tableview 单元格?

这是我迄今为止尝试过的具体事情。

  • 在 Controller #1 中,添加了一个原型单元,将类设置为我的 UITableViewCell 子类,设置重用 id,添加标签并将它们连接到类的出口。在 Controller #2 中,添加了一个空的原型单元,将其设置为相同的类并像以前一样重用 id。当它运行时,当单元格显示在控制器 #2 中时,标签永远不会出现。在控制器 #1 中运行良好。

  • 在不同的 NIB 中设计了每种细胞类型,并连接到适当的细胞类。在情节提要中,添加了一个空原型单元格并设置其类并重用 id 来引用我的单元格类。在控制器的viewDidLoad 方法中,将这些NIB 文件注册为重用ID。显示时,两个控制器中的单元格都是空的,就像原型一样。

  • 将两个控制器中的原型保持为空并设置类并将 id 重用于我的单元类。完全用代码构建单元格的 UI。单元格在所有控制器中都能完美运行。

在第二种情况下,我怀疑原型总是覆盖 NIB,如果我杀死了原型单元,则为我的 NIB 注册重用 id 会起作用。但是我无法设置从单元到其他帧的转场,这确实是使用情节提要的全部意义所在。

归根结底,我想要两件事:在情节提要中连接基于 tableview 的流程,并以可视方式而不是在代码中定义单元格布局。到目前为止,我不知道如何获得这两个。

【问题讨论】:

    标签: objective-c ios uitableview storyboard


    【解决方案1】:

    据我了解,您希望:

    1. 在 IB 中设计一个可用于多个故事板场景的单元。
    2. 从该单元格配置独特的故事板转场,具体取决于单元格所在的场景。

    很遗憾,目前没有办法做到这一点。要了解为什么您以前的尝试不起作用,您需要更多地了解故事板和原型表视图单元格的工作原理。 (如果您不关心为什么这些其他尝试不起作用,请随时离开。除了建议您提交错误之外,我没有任何神奇的解决方法。)

    从本质上讲,故事板只不过是 .xib 文件的集合。当您从情节提要中加载具有一些原型单元格的表格视图控制器时,会发生以下情况:

    • 每个原型单元实际上都是它自己的嵌入式迷你笔尖。因此,当表格视图控制器加载时,它会遍历每个原型单元格的 nib,并调用 -[UITableView registerNib:forCellReuseIdentifier:]。
    • 表格视图向控制器询问单元格。
    • 你可能打电话给-[UITableView dequeueReusableCellWithIdentifier:]
    • 当您请求具有给定重用标识符的单元格时,它会检查它是否已注册 nib。如果是这样,它会实例化该单元格的一个实例。这由以下步骤组成:

      1. 查看单元格的类,如单元格的 nib 中所定义。致电[[CellClass alloc] initWithCoder:]。
      2. -initWithCoder: 方法通过并添加子视图并设置在 nib 中定义的属性。 (IBOutlets 也可能在这里连接,虽然我还没有测试过;它可能发生在 -awakeFromNib)
    • 您可以随意配置单元格。

    这里要注意的重要一点是单元格的类和单元格的视觉外观之间是有区别的。您可以创建同一类的两个单独的原型单元,但它们的子视图布局完全不同。事实上,如果你使用默认的UITableViewCell 样式,这正是正在发生的事情。例如,“默认”样式和“字幕”样式都由相同的UITableViewCell 类表示。

    这很重要:单元格的类与特定的视图层次结构没有一一对应的关系。视图层次结构完全由向此特定控制器注册的原型单元格中的内容决定。

    另外请注意,该细胞的重用标识符未在某些全球细胞药房中注册。重用标识符仅在单个 UITableView 实例的上下文中使用。


    鉴于这些信息,让我们看看您在上述尝试中发生了什么。

    在 Controller #1 中,添加了一个原型单元,将类设置为我的 UITableViewCell 子类,设置重用 id,添加标签和连线 他们到班级的网点。在 Controller #2 中,添加了一个空的 原型单元,将其设置为相同的类并像以前一样重用 id。什么时候 它运行,当单元格显示时,标签永远不会出现 控制器 #2。在控制器 #1 中运行良好。

    这是意料之中的。虽然两个单元具有相同的类,但传递给控制器​​#2 中单元的视图层次结构完全没有子视图。所以你得到了一个空单元格,这正是你在原型中放置的内容。

    在不同的 NIB 中设计每种细胞类型并连接到 适当的细胞类别。在情节提要中,添加了一个空的原型单元格 并设置它的类并重用 id 来引用我的单元类。在 控制器的 viewDidLoad 方法,为 重用标识。显示时,两个控制器中的单元格都是空的,就像 原型。

    同样,这是意料之中的。重用标识符不在情节提要场景或 nib 之间共享,因此所有这些不同的单元格具有相同的重用标识符这一事实毫无意义。您从 tableview 返回的单元格的外观将与情节提要场景中的原型单元格相匹配。

    不过,这个解决方案很接近。正如您所指出的,您可以通过编程方式调用-[UITableView registerNib:forCellReuseIdentifier:],传递包含该单元格的UINib,然后您将返回同一个单元格。 (这并不是因为原型“覆盖”了 nib;您只是没有在 tableview 中注册 nib,所以它仍在查看嵌入在情节提要中的 nib。)不幸的是,这种方法有一个缺陷——没有办法将故事板转场连接到独立 nib 中的单元格。

    将两个控制器中的原型保持为空并设置类和重用 id 到我的单元班。完全用代码构建单元格的 UI。细胞 在所有控制器中都能完美运行。

    当然。希望这并不令人惊讶。


    所以,这就是它不起作用的原因。您可以在独立的笔尖中设计您的单元格,并在多个故事板场景中使用它们;您目前无法将故事板转场连接到这些单元格。不过,希望您在阅读本文的过程中有所收获。

    【讨论】:

    • 啊,我明白了。你解决了我的误解——视图层次结构完全独立于我的类。回想起来很明显!感谢您的出色回答。
    • 似乎不再不可能了:stackoverflow.com/questions/8574188/…
    • @RichApodaca 我在回答中提到了该解决方案。但它不在情节提要中;它在一个单独的笔尖中。所以你不能连接segues或做其他故事板式的事情。因此,它并没有完全解决最初的问题。
    • 从 XCode8 开始,如果您想要仅故事板的解决方案,以下解决方法似乎可行。第 1 步)在 ViewController #1 的 tableview 中创建原型单元格,并与自定义 UITableViewCell 类关联 第 2 步)在 ViewController #2 的 tableview 中复制/粘贴该单元格。随着时间的推移,您必须记住通过删除您在情节提要中制作的副本并粘贴回更新的原型来手动将更新传播到单元格的副本。
    【解决方案2】:

    尽管 BJ Homer 给出了很好的答案,但我觉得我有一个解决方案。就我的测试而言,它有效。

    概念:为 xib 单元创建自定义类。在那里,您可以等待触摸事件并以编程方式执行 segue。现在我们所需要的只是对执行 Segue 的控制器的引用。我的解决办法是设置在tableView:cellForRowAtIndexPath:。

    示例

    我有一个 DetailedTaskCell.xib 包含一个表格单元格,我想在多个表格视图中使用它:

    该单元格有一个自定义类TaskGuessTableCell:

    这就是神奇发生的地方。

    // TaskGuessTableCell.h
    #import <Foundation/Foundation.h>
    
    @interface TaskGuessTableCell : UITableViewCell
    @property (nonatomic, weak) UIViewController *controller;
    @end
    
    // TashGuessTableCell.m
    #import "TaskGuessTableCell.h"
    
    @implementation TaskGuessTableCell
    
    @synthesize controller;
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
    {
        NSIndexPath *path = [controller.tableView indexPathForCell:self];
        [controller.tableView selectRowAtIndexPath:path animated:NO scrollPosition:UITableViewScrollPositionNone];
        [controller performSegueWithIdentifier:@"FinishedTask" sender:controller];
        [super touchesEnded:touches withEvent:event];
    }
    
    @end
    

    我有多个 Segue,但它们都具有相同的名称:"FinishedTask"。如果您需要在这里灵活一些,我建议添加另一个属性。

    ViewController 看起来像这样:

    // LogbookViewController.m
    #import "LogbookViewController.h"
    #import "TaskGuessTableCell.h"
    
    @implementation LogbookViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad]
    
        // register custom nib
        [self.tableView registerNib:[UINib nibWithNibName:@"DetailedTaskCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"DetailedTaskCell"];
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        TaskGuessTableCell *cell;
    
        cell = [tableView dequeueReusableCellWithIdentifier:@"DetailedTaskCell"];
        cell.controller = self; // <-- the line that matters
        // if you added the seque property to the cell class, set that one here
        // cell.segue = @"TheSegueYouNeedToTrigger";
        cell.taskTitle.text  = [entry title];
        // set other outlet values etc. ...
    
        return cell;
    }
    
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    {
        if([[segue identifier] isEqualToString:@"FinishedTask"])
        {
            // do what you have to do, as usual
        }
    
    }
    
    @end
    

    可能有更优雅的方法来实现相同的目标,但是 - 它有效! :)

    【讨论】:

    • 谢谢,我正在我的项目中实施这种方法。您可以改写此方法,这样您就不必自己获取 indexPath 并选择行: -(void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; if(selected) [self.controller performSegueWithIdentifier:self.segue sender:self];我认为 super 在调用 [super touchesEnded:touches withEvent:event]; 时会选择单元格。如果没有,你知道它什么时候被选中吗?
    • 请注意,使用此解决方案,每次触摸在单元格内结束时都会触发 segue。这包括如果您只是滚动单元格,而不是实际尝试选择它。在单元格上覆盖-setSelected: 可能会更好,并且仅在从NO 转换到YES 时触发segue。
    • 我在setSelected:,BJ 上的运气更好。谢谢。的确,这是一个不优雅的解决方案(它感觉错误),但与此同时,它有效,所以我一直在使用它,直到它得到修复(或苹果法庭发生某些变化)。
    【解决方案3】:

    我一直在寻找这个,我找到了 Richard Venable 的 this answer。它对我有用。

    iOS 5 在 UITableView 上包含一个新方法:registerNib:forCellReuseIdentifier:

    要使用它,请将 UITableViewCell 放在笔尖中。它必须是唯一的根 笔尖中的对象。

    你可以在加载你的 tableView 之后注册 nib,然后当你 调用 dequeueReusableCellWithIdentifier: 使用单元标识符,它 将从笔尖中拉出它,就像您使用故事板一样 原型细胞。

    【讨论】:

      【解决方案4】:

      BJ Homer 对正在发生的事情给出了很好的解释。

      从实际的角度来看,我要补充一点,鉴于您不能将单元格作为 xib 并连接 segue,最好的选择是将单元格作为 xib - 转换比单元格布局和属性更容易维护跨越多个地方,无论如何,您的 segue 可能与您的不同控制器不同。您可以直接从表视图控制器定义转场到下一个控制器,并在代码中执行它。 .

      另外需要注意的是,将单元格作为单独的 xib 文件会阻止您将任何操作等直接连接到表视图控制器(无论如何,我还没有解决这个问题 - 您无法定义文件的所有者作为任何有意义的东西)。我正在通过定义单元格的表视图控制器应符合的协议并将控制器添加为类似于委托的弱属性来解决此问题,在 cellForRowAtIndexPath 中。

      【讨论】:

        【解决方案5】:

        斯威夫特 3

        BJ Homer 给出了很好的解释,它帮助我理解了这个概念。到make a custom cell reusable in storyboard,它可以在任何TableViewController 中使用,我们必须以mix the Storyboard and xib 方法。假设我们有一个名为CustomCell 的单元格,它将在TableViewControllerOne 和TableViewControllerTwo 中使用。我正在逐步完成。
        1. 文件 > 新建 > 单击文件 > 选择 Cocoa Touch 类 > 单击下一步 > 为您的班级命名(例如 CustomCell)> 选择子类作为UITableVieCell > 勾选同时创建 XIB 文件复选框,然后按下一步。
        2. 根据需要自定义单元格,并在单元格的属性检查器中设置标识符,这里我们将设置为 CellIdentifier。此标识符将在您的 ViewController 中用于识别和重用 Cell。
        3. 现在我们只需在 ViewController viewDidLoad 中输入 register this cell。不需要任何初始化方法。
        4.现在我们可以在任何tableView中使用这个自定义单元格。

        在 TableViewControllerOne 中

        let reuseIdentifier = "CellIdentifier"
        
        override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: reuseIdentifier)
        } 
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier:reuseIdentifier, for: indexPath) as! CustomCell
            return cell!
        }
        

        【讨论】:

          【解决方案6】:

          我找到了一种方法来为同一个 VC 加载单元格,而不是针对 segues 进行测试。这可能是在单独的笔尖中创建单元格的解决方法

          假设您有一个 VC 和 2 个表格,并且您想在情节提要中设计一个单元格并在两个表格中使用它。

          (例如:一个表和一个带有 UISearchController 的搜索字段以及一个用于结果的表,并且您希望在两者中使用相同的 Cell)

          当控制器请求单元格时,请执行以下操作:

          - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
          {
              static NSString * identifier = @"CELL_ID";
          
              ContactsCell *cell = [self.YOURTABLEVIEW dequeueReusableCellWithIdentifier:identifier];
            // Ignore the "tableView" argument
          }
          

          这里有故事板中的单元格

          【讨论】:

          • 我试过了,它似乎确实有效,但是细胞永远不会被重复使用。系统每次都会创建和释放新的单元格。
          • 这与可以在Adding a Search Bar to a Table View With Storyboards 中找到的建议相同。如果你有兴趣,那里有这个解决方案的更深入的解释(搜索tableView:cellForRowAtIndexPath:)。
          • 但这文字较少,回答了问题
          【解决方案7】:

          如果我正确理解您的问题,这很容易。在您的故事板中创建一个UIViewController,它将保存您的原型单元并创建一个从故事板加载自身的静态共享实例。要处理视图控制器转场,请使用手动转场插座并在表视图委托didSelectRow 上触发(手动转场插座是故事板中视图控制器顶部的中间图标,位于“First Responder”和“Exit”之间)。

          XCode 12.5、iOS 13.6

          // A cell with a single UILabel
          
          class UILabelCell: UITableViewCell {
              @IBOutlet weak var label: UILabel!
          }
          
          // A cell with a signle UISwitch
          
          class UISwitchCell: UITableViewCell {
              @IBOutlet weak var uiSwitch: UISwitch!
          }
          
          
          // The TableViewController to hold the prototype cells.    
          
          class CellPrototypeTableViewController: UITableViewController {
              
              // Loads the view controller from the storyboard
              
              static let shared: CellPrototypeTableViewController = {
                  let storyboard = UIStoryboard(name: "Main", bundle: nil)
                  let viewController = storyboard.instantiateViewController(withIdentifier: "cellProtoypeVC") as! CellPrototypeTableViewController
                  viewController.loadViewIfNeeded()  // Make sure to force view controller to load the view!
                  return viewController
              }()
          
              
              // Helper methods to deque the cells
              
              func dequeUILabeCell() -> UILabelCell {
                  let cell = self.tableView.dequeueReusableCell(withIdentifier: "uiLabelCell") as! UILabelCell
                  return cell
              }
              
              func dequeUISwitchCell() -> UISwitchCell {
                  let cell = self.tableView.dequeueReusableCell(withIdentifier: "uiSwitchCell") as! UISwitchCell
                  return cell
              }
          }
          

          用途:

          class MyTableViewController: UITableViewController {
              
              override func numberOfSections(in tableView: UITableView) -> Int {
                  return 1
              }
              
              override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
                  return 2
              }
              
              override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
                  
                  // Dequeue the cells from the shared instance
                  
                  switch indexPath.row {
                  case 0:
                      let uiLabelCell = CellPrototypeTableViewController.shared.dequeUILabeCell()
                      uiLabelCell.label.text = "Hello World"
                      return uiLabelCell
                  case 1:
                      let uiSwitchCell = CellPrototypeTableViewController.shared.dequeUISwitchCell()
                      uiSwitchCell.uiSwitch.isOn = false
                      return uiSwitchCell
                  default:
                      fatalError("IndexPath out of bounds")
                  }
              }
              
              // Handling Segues
              
              override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
                  switch indexPath.row {
                  case 0: self.performSegue(withIdentifier: "first", sender: nil)
                  case 1: self.performSegue(withIdentifier: "second", sender: nil)
                  default:
                      fatalError("IndexPath out of bounds")
                  }
              }
          }
          

          【讨论】:

            猜你喜欢
            • 2014-04-01
            • 1970-01-01
            • 1970-01-01
            • 2017-12-16
            • 1970-01-01
            • 1970-01-01
            • 2012-11-01
            • 2014-02-05
            • 1970-01-01
            相关资源
            最近更新 更多