在故事板中,如何制作用于多个控制器的自定义单元?

时间:2012-02-12 02:37:24

标签: objective-c ios uitableview storyboard

我正在尝试在我正在使用的应用中使用故事板。在应用中有列表用户,每个都包含其他列表(列表成员,用户拥有的列表)。因此,我有ListCellUserCell个类。目标是让它们在整个应用程序中可重复使用(即,在我的任何tableview控制器中)。

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

如何在故事板中创建可在任何视图控制器中重复使用的自定义tableview单元格?

以下是我迄今为止尝试过的具体事项。

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

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

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

在第二种情况下,我怀疑原型总是覆盖NIB,如果我杀死了原型单元,注册我的NIB以获得重用ID就行了。但是后来我无法设置从单元格到其他框架的segue,这实际上是使用故事板的重点。

在一天结束时,我想要两件事:在故事板中连接基于tableview的流,并在视觉上而不是在代码中定义单元格布局。到目前为止,我无法看到如何获得这两者。

7 个答案:

答案 0 :(得分:204)

据我所知,你想:

  1. 在IB中设计一个可用于多个故事板场景的单元格。
  2. 根据单元格所在的场景,从该单元格配置唯一的故事板segue。
  3. 不幸的是,目前无法做到这一点。要了解您之前的尝试无效的原因,您需要了解有关故事板和原型表视图单元格如何工作的更多信息。 (如果你不关心为什么这些其他尝试都没有用,请随时离开。我没有为你提供神奇的解决方法,除了暗示你提交了一个错误。)

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

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

      1. 查看单元格的类,如单元格中的定义。致电[[CellClass alloc] initWithCoder:]
      2. -initWithCoder:方法遍历并添加子视图并设置在nib中定义的属性。 (IBOutlet也可能会被吸引到这里,虽然我还没有测试过;它可能发生在-awakeFromNib
    • 您可以根据需要配置您的单元格。

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

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

    同样注意,单元的重用标识符未在某个全局单元诊所中注册。重用标识符仅在单个UITableView实例的上下文中使用。


    根据这些信息,让我们看一下上述尝试中发生的事情。

      

    在Controller#1中,添加了一个原型单元格,将类设置为my   UITableViewCell子类,设置重用ID,添加标签和有线   他们到了班级的分店。在Controller#2中,添加了一个空   原型单元格,将其设置为同一个类并像以前一样重用id。什么时候   它会运行,当显示细胞时标签永远不会出现   控制器#2。在Controller#1中正常工作。

    这是预期的。虽然两个单元格具有相同的类,但传递给Controller#2中的单元格的视图层次结构完全没有子视图。所以你得到一个空单元格,这正是你在原型中放入的单元。

      

    在不同的NIB中设计每个单元类型并连接到   适当的细胞类。在故事板中,添加了一个空的原型单元格   并设置其类并重用id来引用我的单元类。在   控制器' viewDidLoad方法,注册了那些NIB文件   重用id。如图所示,两个控制器中的单元格都是空的   原型。

    同样,这是预期的。故障板场景或笔尖之间不共享重用标识符,因此所有这些不同的单元具有相同的重用标识符的事实是没有意义的。从tableview返回的单元格将具有与故事板场景中的原型单元格匹配的外观。

    但这个解决方案很接近。如您所述,您可以通过编程方式调用-[UITableView registerNib:forCellReuseIdentifier:],传递包含该单元格的UINib,然后您返回相同的单元格。 (这不是因为原型是"覆盖" nib;你根本没有用tableview注册nib,所以它仍然在查看故事板中嵌入的nib。)不幸的是,这种方法存在缺陷 - 没有办法将故事板segue连接到独立笔尖中的单元格。

      

    将两个控制器中的原型保持为空并设置类并重用id   到我的细胞班。构建细胞' UI完全在代码中。细胞   在所有控制器中完美地工作。

    自然。希望这不足为奇。


    那么,这就是为什么它没有奏效。您可以在独立的笔尖中设计单元格,并在多个故事板场景中使用它们;你现在无法将故事板segue连接到那些单元格。但是,希望你在阅读这篇文章的过程中学到了一些东西。

答案 1 :(得分:58)

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

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

实施例

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

DetailedTaskCell.xib

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

enter image description here

这就是魔术发生的地方。

// 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

我有多个Segues,但它们都有相同的名称:"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

可能有更优雅的方法来达到同样的目的但是 - 它有效! :)

答案 2 :(得分:16)

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

  

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

     

要使用它,请在笔尖中放置UITableViewCell。它必须是唯一的根   笔尖中的物体。

     

您可以在加载tableView后注册nib,然后再注册   call dequeueReusableCellWithIdentifier:带有单元标识符,它   将它从笔尖拉出来,就像你使用了故事板一样   原型细胞。

答案 3 :(得分:10)

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

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

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

答案 4 :(得分:10)

Swift 3

BJ荷马给出了一个很好的解释,它帮助我理解了这个概念。对于make a custom cell reusable in storyboard,可以在任何TableViewController中使用,我们必须mix the Storyboard and xib方法。假设我们有一个名为CustomCell的单元格,将在TableViewControllerOneTableViewControllerTwo中使用。我正在分步进行。
1。文件&gt;新&gt;点击文件&gt;选择Cocoa Touch Class&gt;单击“下一步”>提供班级名称(例如CustomCell)&gt;选择Subclass作为UITableVieCell&gt;勾选“创建XIB文件”复选框,然后按“下一步” 2. 。根据需要自定义单元格,并在属性检查器中为单元格设置标识符,此处我们将设置为CellIdentifier。此标识符将在ViewController中用于标识和重用Cell 3。现在我们只需要在ViewController register this cellviewDidLoad。无需任何初始化方法。
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!
}

答案 5 :(得分:5)

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

假设您有一个VC和2个表,并且您想在故事板中设计一个单元格并在两个表中使用它。

(例如:一个表和一个带有UISearchController的搜索字段,其中包含结果表,并且您希望在两者中使用相同的单元格)

当控制器要求单元格执行此操作时:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString * identifier = @"CELL_ID";

    ContactsCell *cell = [self.YOURTABLEVIEW dequeueReusableCellWithIdentifier:identifier];
  // Ignore the "tableView" argument
}

在这里,你有来自故事板的单元格

答案 6 :(得分:0)

如果我正确理解您的问题,这很容易。在您的故事板中创建一个 UIViewController 来保存您的原型单元格,并创建一个从故事板加载自身的静态共享实例。要处理视图控制器转场,请使用手动转场出口并在表视图委托 didSelectRow 上触发(手动转场出口是故事板中视图控制器顶部的中间图标,在“第一响应者”和“退出').

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")
        }
    }
}