Создание многоразового 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 наконец-то добавила Ссылки на раскадровки, сделав это намного проще.)
Здесь типичная раскадровка с видами контейнера повсюду. Все виды контейнеров. Это просто, как вы делаете приложения.
(Как любопытство, ответ 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, используя выходные/действия и т.д. после установки владельца файла представления на наш Пользовательский класс:
Видео, демонстрирующее реализацию такого класса представлений шаг за шагом, используя этот подход, можно найти в следующем видео.
Ответ 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".
Плюсы:
Минусы:
- Просто не работает:
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:
Раскадровка:
Результат:
Ответ 5
Не забывайте
Два важных момента:
- Установите владельца файла .xib в имя класса вашего пользовательского представления.
- Не устанавливайте имя пользовательского класса в IB для корневого представления .xib.
Я несколько раз приходил к этой странице Q & A, изучая возможность повторного использования. Забыв вышеприведенные моменты, я потратил много времени на то, чтобы выяснить, что вызывает бесконечную рекурсию. Эти пункты упоминаются в других ответах здесь и в других местах, но я просто хочу их повторить.
Мой полный быстрый ответ с шагами здесь.
Ответ 6
Существует решение, которое намного более чистое, чем решения выше:
https://www.youtube.com/watch?v=xP7YvdlnHfA
Нет свойств Runtime, нет проблемы с рекурсивным вызовом.
Я попробовал, и он работал как прелесть, используя раскадровку и XIB с помощью свойств IBOutlet (iOS8.1, XCode6).
Удачи вам в кодировании!