Создание многоразового UIView с xib (и загрузка из раскадровки)
Хорошо, в Stackru есть десятки сообщений об этом, но ни одно из них не совсем ясно в отношении решения. Я хотел бы создать кастом UIView
с сопроводительным XIB-файлом. Требования следующие:
- Нет отдельного
UIViewController
- полностью автономный класс - Розетки в классе, которые позволяют мне устанавливать / получать свойства вида
Мой текущий подход к этому:
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; }
Создавать программно используя
-(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]
и простая установка объекта с использованием содержимого загруженного пера кажется немного подозрительной - здесь есть совет, чтобы добавить подпредставление в этом случае, которое также работает нормально). Тем не менее, я хотел бы иметь возможность создавать представление и из раскадровки. Так что я могу:
- Поместите
UIView
на родительском представлении в раскадровке - Установите его пользовательский класс в
MyCustomView
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 имя класса вашего пользовательского представления.
- Не устанавливайте пользовательское имя класса в IB для корневого представления.xib.
Я заходил на эту страницу вопросов и ответов несколько раз, пока учился делать многоразовые просмотры. Забывание вышеупомянутых пунктов заставило меня потратить впустую много времени, пытаясь выяснить, что вызывало бесконечную рекурсию. Эти вопросы упоминаются в других ответах здесь и в других местах, но я просто хочу еще раз подчеркнуть их здесь.
Мой полный ответ Swift с шагами здесь.
Существует решение, которое намного чище, чем приведенные выше: https://www.youtube.com/watch?v=xP7YvdlnHfA
Нет свойств времени выполнения, нет проблемы рекурсивного вызова вообще. Я попробовал это, и это работало как очарование, используя раскадровку и XIB со свойствами IBOutlet (iOS8.1, XCode6).
Удачи в кодировании!