Как загрузить пользовательские UITableViewCells из файлов Xib? - PullRequest
288 голосов
/ 12 февраля 2009

Вопрос прост: как загрузить пользовательские UITableViewCell из файлов Xib? Это позволяет вам использовать Interface Builder для проектирования ваших ячеек. Ответ очевидно не прост из-за проблем управления памятью. В этой теме упоминается проблема и предлагается решение, но она выпущена до выпуска NDA и не содержит кода. Вот длинная нить , в которой обсуждается проблема без однозначного ответа.

Вот код, который я использовал:

static NSString *CellIdentifier = @"MyCellIdentifier";

MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CellIdentifier owner:self options:nil];
    cell = (MyCell *)[nib objectAtIndex:0];
}

Чтобы использовать этот код, создайте MyCell.m / .h, новый подкласс UITableViewCell и добавьте IBOutlets для компонентов, которые вы хотите. Затем создайте новый файл «Пустой XIB». Откройте файл Xib в IB, добавьте объект UITableViewCell, установите для его идентификатора значение «MyCellIdentifier», установите для его класса значение MyCell и добавьте свои компоненты. Наконец, подключите IBOutlets к компонентам. Обратите внимание, что мы не установили владельца файла в IB.

Другие методы рекомендуют устанавливать владельца файла и предупреждать об утечках памяти, если Xib не загружается через дополнительный фабричный класс. Я проверил вышеизложенное в разделе «Инструменты / утечки» и не обнаружил утечек памяти.

Так каков канонический способ загрузки ячеек из Xibs? Мы устанавливаем владельца файла? Нужен ли нам завод? Если да, то как выглядит код фабрики? Если есть несколько решений, давайте выясним плюсы и минусы каждого из них ...

Ответы [ 23 ]

299 голосов
/ 29 ноября 2012

Правильное решение таково:

- (void)viewDidLoad
{
    [super viewDidLoad];
    UINib *nib = [UINib nibWithNibName:@"ItemCell" bundle:nil];
    [[self tableView] registerNib:nib forCellReuseIdentifier:@"ItemCell"];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Create an instance of ItemCell
    PointsItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ItemCell"];

    return cell;
}
287 голосов
/ 21 декабря 2009

Вот два метода, которые автор оригинальной версии рекомендовал инженеру IB .

См. Фактическое сообщение для более подробной информации. Я предпочитаю метод № 2, так как он кажется более простым.

Метод № 1:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BDCustomCell"];
    if (cell == nil) {
        // Create a temporary UIViewController to instantiate the custom cell.
        UIViewController *temporaryController = [[UIViewController alloc] initWithNibName:@"BDCustomCell" bundle:nil];
        // Grab a pointer to the custom cell.
        cell = (BDCustomCell *)temporaryController.view;
        [[cell retain] autorelease];
        // Release the temporary UIViewController.
        [temporaryController release];
    }

    return cell;
}

Метод № 2:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BDCustomCell"];
    if (cell == nil) {
        // Load the top-level objects from the custom cell XIB.
        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"BDCustomCell" owner:self options:nil];
        // Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
        cell = [topLevelObjects objectAtIndex:0];
    }

    return cell;
}

Обновление (2014): Метод № 2 все еще действителен, но документации для него больше нет. Раньше он был в официальных документах , но теперь удален в пользу раскадровок.

Я опубликовал рабочий пример на Github:
https://github.com/bentford/NibTableCellExample

редактировать для Swift 4.2

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
    self.tblContacts.register(UINib(nibName: CellNames.ContactsCell, bundle: nil), forCellReuseIdentifier: MyIdentifier)
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: MyIdentifier, for: indexPath) as! ContactsCell

    return cell
}
38 голосов
/ 21 мая 2015

Регистрация

После iOS 7 этот процесс был упрощен до ( swift 3.0 ):

// For registering nib files
tableView.register(UINib(nibName: "MyCell", bundle: Bundle.main), forCellReuseIdentifier: "cell")

// For registering classes
tableView.register(MyCellClass.self, forCellReuseIdentifier: "cell")

( Примечание ) Этого также можно достичь, создав ячейки в файлах .xib или .stroyboard в качестве ячеек-прототипов. Если вам нужно присоединить к ним класс, вы можете выбрать прототип ячейки и добавить соответствующий класс (конечно, должен быть потомком UITableViewCell).

Dequeue

и позже, исключено из использования ( swift 3.0 ):

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
    let cell : UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

    cell.textLabel?.text = "Hello"

    return cell
}

Разница в том, что этот новый метод не только удаляет ячейку, но и создает ее, если она не существует (это означает, что вам не нужно делать if (cell == nil) махинации), и ячейка готова к использованию так же, как в пример выше.

( Предупреждение ) tableView.dequeueReusableCell(withIdentifier:for:) имеет новое поведение, если вы вызываете другое (без indexPath:), вы получаете старое поведение, в котором вам нужно проверить nil и экземпляр Сделайте это сами, обратите внимание на UITableViewCell? возвращаемое значение.

if let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? MyCellClass
{
    // Cell be casted properly
    cell.myCustomProperty = true
}
else
{
    // Wrong type? Wrong identifier?
}

И, конечно, тип связанного класса ячейки тот, который вы определили в файле .xib для подкласса UITableViewCell, или, альтернативно, с помощью другого метода регистра.

Конфигурация

В идеале, ваши ячейки уже были настроены с точки зрения внешнего вида и позиционирования контента (например, меток и представлений изображений) к моменту их регистрации, а с помощью метода cellForRowAtIndexPath вы просто заполняете их.

Все вместе

class MyCell : UITableViewCell
{
    // Can be either created manually, or loaded from a nib with prototypes
    @IBOutlet weak var labelSomething : UILabel? = nil
}

class MasterViewController: UITableViewController 
{
    var data = ["Hello", "World", "Kinda", "Cliche", "Though"]

    // Register
    override func viewDidLoad()
    {
        super.viewDidLoad()

        tableView.register(MyCell.self, forCellReuseIdentifier: "mycell")
        // or the nib alternative
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        return data.count
    }

    // Dequeue
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
    {
        let cell = tableView.dequeueReusableCell(withIdentifier: "mycell", for: indexPath) as! MyCell

        cell.labelSomething?.text = data[indexPath.row]

        return cell
    }
}

И, конечно, все это доступно в ObjC с такими же именами.

33 голосов
/ 22 июня 2010

Взял ответ Шона Крейвера и немного его почистил.

BBCell.h:

#import <UIKit/UIKit.h>

@interface BBCell : UITableViewCell {
}

+ (BBCell *)cellFromNibNamed:(NSString *)nibName;

@end

BBCell.m:

#import "BBCell.h"

@implementation BBCell

+ (BBCell *)cellFromNibNamed:(NSString *)nibName {
    NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:NULL];
    NSEnumerator *nibEnumerator = [nibContents objectEnumerator];
    BBCell *customCell = nil;
    NSObject* nibItem = nil;
    while ((nibItem = [nibEnumerator nextObject]) != nil) {
        if ([nibItem isKindOfClass:[BBCell class]]) {
            customCell = (BBCell *)nibItem;
            break; // we have a winner
        }
    }
    return customCell;
}

@end

Я делаю все мои подклассы UITableViewCell BBCell, а затем заменяю стандарт

cell = [[[BBDetailCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"BBDetailCell"] autorelease];

с:

cell = (BBDetailCell *)[BBDetailCell cellFromNibNamed:@"BBDetailCell"];
16 голосов
/ 20 марта 2012

Я использовал метод Бентфорда * 2 :

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BDCustomCell"];
    if (cell == nil) {
        // Load the top-level objects from the custom cell XIB.
        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"BDCustomCell" owner:self options:nil];
        // Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
        cell = [topLevelObjects objectAtIndex:0];
    }

    return cell;
}

Это работает, но следите за соединениями с Владельцем файла в вашем пользовательском файле .ITIB UITableViewCell.

Передав owner:self в своем выражении loadNibNamed, вы устанавливаете UITableViewController в качестве владельца файла вашего UITableViewCell.

Если вы перетащите файл заголовка в IB, чтобы настроить действия и выходы, он по умолчанию настроит их как владельца файла.

В loadNibNamed:owner:options код Apple попытается установить свойства вашего UITableViewController, так как это владелец. Но эти свойства там не определены, поэтому вы получаете сообщение о том, что соответствует значению ключа :

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason:     '[<MyUITableViewController 0x6a383b0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key myLabel.'

Если вместо этого инициируется событие, вы получите NSInvalidArgumentException:

-[MyUITableViewController switchValueDidChange:]: unrecognized selector sent to instance 0x8e9acd0
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyUITableViewController switchValueDidChange:]: unrecognized selector sent to instance 0x8e9acd0'
*** First throw call stack:
(0x1903052 0x15eed0a 0x1904ced 0x1869f00 0x1869ce2 0x1904ec9 0x5885c2 0x58855a 0x62db76 0x62e03f 0x77fa6c 0x24e86d 0x18d7966 0x18d7407 0x183a7c0 0x1839db4 0x1839ccb 0x1f8b879 0x1f8b93e 0x585a9b 0xb904d 0x2c75)
terminate called throwing an exceptionCurrent language:  auto; currently objective-c

Простой обходной путь - указать ваши соединения Interface Builder на UITableViewCell вместо владельца файла:

  1. Щелкните правой кнопкой мыши по Владельцу файла, чтобы открыть список подключений
  2. Сделайте снимок экрана с помощью Command-Shift-4 (перетащите, чтобы выбрать область для захвата)
  3. x подключения от владельца файла
  4. Щелкните правой кнопкой мыши UITableCell в иерархии объектов и заново добавьте соединения.
14 голосов
/ 26 августа 2013

Я решил опубликовать, так как мне не нравится ни один из этих ответов - все всегда может быть проще, и это, безусловно, самый лаконичный способ, который я нашел.

1. Создайте свою Xib в Интерфейсном Разработчике, как вам нравится

  • Установить для владельца файла класс NSObject
  • Добавьте UITableViewCell и установите его класс в MyTableViewCellSubclass - если ваш IB падает (случается в Xcode> 4 на момент написания этой статьи), просто используйте UIView для создания интерфейса в Xcode 4, если у вас все еще есть его расположение
  • Расположите ваши подпредставления внутри этой ячейки и присоедините ваши соединения IBOutlet к вашему @interface в .h или .m (.m - мои предпочтения)

2. В вашем подклассе UIViewController или UITableViewController

@implementation ViewController

static NSString *cellIdentifier = @"MyCellIdentier";

- (void) viewDidLoad {

    ...
    [self.tableView registerNib:[UINib nibWithNibName:@"MyTableViewCellSubclass" bundle:nil] forCellReuseIdentifier:cellIdentifier];
}

- (UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyTableViewCellSubclass *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    ...

    return cell;
}

3. В вашем MyTableViewCellSubclass

- (id) initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        ...
    }

    return self;
}
9 голосов
/ 28 апреля 2010

Если вы используете Interface Builder для создания ячеек, убедитесь, что вы установили Идентификатор в Инспекторе. Затем проверьте, что это то же самое при вызове dequeueReusableCellWithIdentifier.

Я случайно забыл установить некоторые идентификаторы в настольном проекте, и изменение производительности было похоже на ночь и день.

8 голосов
/ 12 февраля 2009

Загрузка UITableViewCells из XIBs экономит много кода, но обычно приводит к ужасной скорости прокрутки (на самом деле это не XIB, а чрезмерное использование UIViews, которое вызывает это).

Я предлагаю вам взглянуть на это: Ссылка ссылка

6 голосов
/ 12 февраля 2009

Вот метод класса, который я использовал для создания пользовательских ячеек из XIB:

+ (CustomCell*) createNewCustomCellFromNib {

    NSArray* nibContents = [[NSBundle mainBundle]
                            loadNibNamed:@"CustomCell" owner:self options:NULL];

    NSEnumerator *nibEnumerator = [nibContents objectEnumerator];
    CustomCell *customCell= nil;
    NSObject* nibItem = nil;

    while ( (nibItem = [nibEnumerator nextObject]) != nil) {

        if ( [nibItem isKindOfClass: [CustomCell class]]) {
            customCell = (CustomCell*) nibItem;

            if ([customCell.reuseIdentifier isEqualToString: @"CustomCell"]) {
                break; // we have a winner
            }
            else
                fuelEntryCell = nil;
        }
    }
    return customCell;
}

Затем в XIB я устанавливаю имя класса и повторно использую идентификатор. После этого я могу просто вызвать этот метод в моем контроллере представления вместо

[[UITableViewCell] alloc] initWithFrame:]

Это достаточно быстро и используется в двух моих приложениях доставки. Это более надежно, чем вызов [nib objectAtIndex:0], и, по крайней мере, на мой взгляд, более надежно, чем пример Стефана Бурло, потому что вы гарантированно получите только представление из XIB правильного типа.

5 голосов
/ 07 сентября 2016

Правильное решение это

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.tableView registerNib:[UINib nibWithNibName:@"CustomCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"CustomCell"];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell  *cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell"];
    return cell; 
    }
...