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

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

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

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

  • Переопределить -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  • Проигрывать программно с помощью -(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] и просто устанавливать объект, используя содержимое загруженного nib, кажется немного подозрительным - здесь есть совет добавить subview в этом case, который также отлично работает). Тем не менее, я хотел бы также создать экземпляр представления из раскадровки. Поэтому я могу:

  • Поместите a UIView в родительском представлении в раскадровке
  • Задайте свой собственный класс MyCustomView
  • Переопределить -(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 и выполнять там загрузку nib, но это кажется субоптимальным для меня, поскольку для этого требуется добавить другой файл, как оболочку

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

Ответ 1

Ваша проблема заключается в вызове 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;
}

Ответ 2

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

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

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

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

enter image description here

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

Ответ 3

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

В результате нет никакой возможности, кроме как добавить представление, загруженное из файла xib, в качестве подзадачи пользовательского подкласса UIView, а не полностью заменить его. Это аналогично второму подходу, изложенному в первоначальном вопросе. Грубая схема класса в 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 с макетом пользовательского интерфейса таким образом, который работает последовательно как для раскадровки, так и для кода) - замена опциона self в инициализаторе до того, как он никогда не казался особенно интерпретируемым способом делать что-то, хотя по существу два класса представления для представления тоже не выглядят так здорово.

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

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

Setting the file owner property of the custom view

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

Ответ 4

STEP1. Замена self из раскадровки

Замена метода self in initWithCoder: завершится с ошибкой.

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

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

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

STEP2. Предотвращение рекурсивного вызова

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

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

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

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

Плюсы:

  • None

Минусы:

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

b) Убедитесь, что self имеет какие-либо подпункты

Обычно у вас есть subviews в 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];
    }
}

Плюсы:

  • Никакой трюк в Interface Builder.

Минусы:

  • У вас не может быть subviews в вашей раскадровке.

c) Установите статический флаг во время вызова 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;
    }
}

Плюсы:

  • Простой
  • Никакой трюк в Interface Builder.

Минусы:

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

d) Использовать частный подкласс в 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
  • относительно больше кодов

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

Аналогично 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

Плюсы:

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

Минусы:

  • Необходимо использовать подкласс в раскадровке.

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

STEP3. Свойства копирования

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

- (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 screenshot

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

Storyboard

Результат:

enter image description here

Ответ 5

Не забывайте

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

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

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

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

Ответ 6

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

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

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