Создание многоразового UIView с xib (и загрузка из раскадровки)

Хорошо, в Stackru есть десятки сообщений об этом, но ни одно из них не совсем ясно в отношении решения. Я хотел бы создать кастом UIView с сопроводительным XIB-файлом. Требования следующие:

  • Нет отдельного UIViewController - полностью автономный класс
  • Розетки в классе, которые позволяют мне устанавливать / получать свойства вида

Мой текущий подход к этому:

  1. Override -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. Создавать программно используя -(id)initWithFrame: на мой взгляд контроллер

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

Это работает нормально (хотя никогда не звонит [super init] и простая установка объекта с использованием содержимого загруженного пера кажется немного подозрительной - здесь есть совет, чтобы добавить подпредставление в этом случае, которое также работает нормально). Тем не менее, я хотел бы иметь возможность создавать представление и из раскадровки. Так что я могу:

  1. Поместите UIView на родительском представлении в раскадровке
  2. Установите его пользовательский класс в MyCustomView
  3. Override -(id)initWithCoder: - код, который я видел чаще всего, соответствует такой схеме:

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

Конечно, это не работает, независимо от того, использую ли я описанный выше подход или создаю экземпляр программно, оба заканчивают тем, что рекурсивно вызывают -(id)initWithCoder: при входе -(void)initializeSubviews и загрузка пера из файла.

Несколько других SO вопросов касаются этого, таких как здесь, здесь, здесь и здесь. Однако ни один из приведенных ответов удовлетворительно не решает проблему:

  • Распространенным предложением является встраивание всего класса в UIViewController и выполнение загрузки пера туда, но это кажется мне неоптимальным, так как требует добавления другого файла в качестве оболочки

Может ли кто-нибудь дать совет о том, как решить эту проблему, и получить рабочие места в обычай UIView с минимальной суетой / без тонкой оболочки контроллера? Или есть альтернативный, более чистый способ сделать что-то с минимальным стандартным кодом?

6 ответов

Решение

Ваша проблема звонит loadNibNamed: от (потомок) initWithCoder:, loadNibNamed: внутренние звонки initWithCoder:, Если вы хотите переопределить кодировщик раскадровки и всегда загружать реализацию xib, я предлагаю следующий метод. Добавьте свойство в свой класс представления и в файле xib установите для него предварительно определенное значение (в Определенных пользователем атрибутах времени выполнения). Теперь после звонка [super initWithCoder:aDecoder]; проверить стоимость имущества. Если это предопределенное значение, не вызывайте [self initializeSubviews];,

Итак, как то так:

-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    {
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    }

    return self;
}

-(instancetype)initializeSubviews {
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;
}

Обратите внимание, что этот QA (как и многие) действительно представляет исторический интерес.

В настоящее время в течение многих лет в iOS все выглядит просто как контейнер. Полный учебник здесь

(Действительно, Apple наконец-то добавила Storyboard References, некоторое время назад, сделав это намного проще.)

Вот типичная раскадровка с видом на контейнер повсюду. Все в контейнере. Это просто, как вы делаете приложения.

введите описание изображения здесь

(Любопытно, что ответ KenC показывает, как именно это делалось для загрузки xib в своего рода представление-обертку, поскольку вы не можете "назначить себя".)

Я добавляю это как отдельный пост, чтобы обновить ситуацию с выпуском Swift. Подход, описанный LeoNatan, прекрасно работает в Objective-C. Однако более строгие проверки времени компиляции предотвращают self присваивается при загрузке из файла XIB в Swift.

В результате нет другого выбора, кроме как добавить представление, загруженное из файла xib, в качестве подпредставления пользовательского подкласса UIView, вместо полной замены self. Это аналогично второму подходу, изложенному в первоначальном вопросе. Примерный план класса в Swift с использованием этого подхода выглядит следующим образом:

@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initializeSubviews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initializeSubviews()
    }

    func initializeSubviews() {
        // below doesn't work as returned class name is normally in project module scope
        /*let viewName = NSStringFromClass(self.classForCoder)*/
        let viewName = "ExampleView"
        let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                               owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }

}

Недостатком этого подхода является введение дополнительного избыточного уровня в иерархию представлений, который не существует при использовании подхода, описанного LeoNatan в Objective-C. Тем не менее, это может быть воспринято как необходимое зло и продукт фундаментального способа, которым все спроектировано в XCode (мне все еще кажется сумасшедшим, что так сложно связать пользовательский класс UIView с макетом UI способом, который работает согласованно над раскадровками и из кода) - замена self Оптовая продажа в инициализаторе никогда не казалась особенно понятным способом ведения дел, хотя, по сути, наличие двух классов представлений для представления тоже не кажется таким уж большим.

Тем не менее, одним из приятных результатов этого подхода является то, что нам больше не нужно устанавливать пользовательский класс представления для нашего файла классов в конструкторе интерфейса, чтобы гарантировать правильное поведение при назначении selfи так рекурсивный вызов init(coder aDecoder: NSCoder) при выдаче loadNibNamed() не работает (не устанавливая пользовательский класс в xib-файле, init(coder aDecoder: NSCoder) вместо простой пользовательской версии будет вызываться простой ванильный UIView).

Несмотря на то, что мы не можем выполнить настройку класса для представления, хранящегося непосредственно в xib, мы все же можем связать представление с нашим "родительским" подклассом UIView, используя выходы / действия и т. Д. После установки владельца файла представления для нашего пользовательского класса:

Установка свойства владельца файла пользовательского представления

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

ШАГ 1. Замена self из раскадровки

Замена self в initWithCoder: метод потерпит неудачу со следующей ошибкой.

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'

Вместо этого вы можете заменить декодированный объект awakeAfterUsingCoder: (не awakeFromNib). лайк:

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

ШАГ 2. Предотвращение рекурсивного вызова

Конечно, это также вызывает рекурсивную проблему вызова. (расшифровка раскадровки -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: ->...)
Таким образом, вы должны проверить текущий awakeAfterUsingCoder: вызывается в процессе декодирования раскадровки или в процессе декодирования XIB. У вас есть несколько способов сделать это:

а) использовать личное @property который установлен только в NIB.

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end

и установите "Определенные пользователем атрибуты времени выполнения" только в "MyCustomView.xib".

Плюсы:

  • Никто

Минусы:

  • Просто не работает setXib: будет называться ПОСЛЕ awakeAfterUsingCoder:

б) Проверьте, если self есть какие-либо подвиды

Обычно у вас есть подпредставления в XIB, но не в раскадровке.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(self.subviews.count > 0) {
        // loading xib
        return self;
    }
    else {
        // loading storyboard
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
}

Плюсы:

  • Нет хитрости в Интерфейсном Разработчике.

Минусы:

  • Вы не можете иметь подпредставления в вашей раскадровке.

в) Установите статический флаг во время loadNibNamed: вызов

static BOOL _loadingXib = NO;

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(_loadingXib) {
        // xib
        return self;
    }
    else {
        // storyboard
        _loadingXib = YES;
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        _loadingXib = NO;
        return view;
    }
}

Плюсы:

  • просто
  • Нет хитрости в Интерфейсном Разработчике.

Минусы:

  • Не безопасно: статический общий флаг опасен

г) Использовать закрытый подкласс в XIB

Например, объявить _NIB_MyCustomView как подкласс MyCustomView, И использовать _NIB_MyCustomView вместо MyCustomView только в вашей XIB.

MyCustomView.h:

@interface MyCustomView : UIView
@end

MyCustomView.m:

#import "MyCustomView.h"

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In Storyboard decoding path.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

@interface _NIB_MyCustomView : MyCustomView
@end

@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In XIB decoding path.
    // Block recursive call.
    return self;
}
@end

Плюсы:

  • Нет явного if в MyCustomView

Минусы:

  • Приставка _NIB_ трюк в Xib Interface Builder
  • относительно больше кодов

д) Использовать подкласс в качестве заполнителя в раскадровке

Похожий на d) но используйте подкласс в Storyboard, оригинальный класс в XIB.

Здесь мы заявляем MyCustomViewProto как подкласс MyCustomView,

@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In storyboard decoding
    // Returns MyCustomView loaded from NIB.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

Плюсы:

  • Очень безопасно
  • Чистый; Нет дополнительного кода в MyCustomView,
  • Нет явного if проверить так же, как d)

Минусы:

  • Нужно использовать подкласс в раскадровке.

Я думаю e) это самая безопасная и чистая стратегия. Таким образом, мы принимаем это здесь.

ШАГ 3. Копировать свойства

После loadNibNamed: в 'awakeAfterUsingCoder:', вы должны скопировать несколько свойств из self который является декодированным экземпляром для раскадровки. frame и свойства autolayout / autoresize особенно важны.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                       owner:nil
                                                     options:nil] objectAtIndex:0];
    // copy layout properities.
    view.frame = self.frame;
    view.autoresizingMask = self.autoresizingMask;
    view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

    // copy autolayout constraints
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in self.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == self) firstItem = view;
        if(secondItem == self) secondItem = view;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }

    // move subviews
    for(UIView *subview in self.subviews) {
        [view addSubview:subview];
    }
    [view addConstraints:constraints];

    // Copy more properties you like to expose in Storyboard.

    return view;
}

ОКОНЧАТЕЛЬНОЕ РЕШЕНИЕ

Как видите, это немного стандартного кода. Мы можем реализовать их как "категорию". Здесь я расширяю часто используемые UIView+loadFromNib код.

#import <UIKit/UIKit.h>

@interface UIView (loadFromNib)
@end

@implementation UIView (loadFromNib)

+ (id)loadFromNib {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}

- (void)copyPropertiesFromPrototype:(UIView *)proto {
    self.frame = proto.frame;
    self.autoresizingMask = proto.autoresizingMask;
    self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in proto.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == proto) firstItem = self;
        if(secondItem == proto) secondItem = self;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }
    for(UIView *subview in proto.subviews) {
        [self addSubview:subview];
    }
    [self addConstraints:constraints];
}

Используя это, вы можете объявить MyCustomViewProto лайк:

@interface MyCustomViewProto : MyCustomView
@end

@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    MyCustomView *view = [MyCustomView loadFromNib];
    [view copyPropertiesFromPrototype:self];

    // copy additional properties as you like.

    return view;
}
@end

XIB:

Скриншот XIB

Раскадровка:

Раскадровка

Результат:

Не забывай

Два важных момента:

  1. Установите в качестве владельца файла.xib имя класса вашего пользовательского представления.
  2. Не устанавливайте пользовательское имя класса в IB для корневого представления.xib.

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

Мой полный ответ Swift с шагами здесь.

Существует решение, которое намного чище, чем приведенные выше: https://www.youtube.com/watch?v=xP7YvdlnHfA

Нет свойств времени выполнения, нет проблемы рекурсивного вызова вообще. Я попробовал это, и это работало как очарование, используя раскадровку и XIB со свойствами IBOutlet (iOS8.1, XCode6).

Удачи в кодировании!

Другие вопросы по тегам