Создание многоразового 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] и просто установка объекта с использованием содержимого загруженного пера кажется немного подозрительной – здесь есть совет добавить подвид в этом случае, который также отлично работает). Тем не менее, я хотел бы иметь возможность создать экземпляр представления из раскадровки также. Так Что Я может:
- место
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 и выполнить загрузку пера там, но это кажется мне неоптимальным, поскольку для этого требуется добавить еще один файл как обертку
может кто-нибудь дать совет о том, как решить эту проблему, и получить рабочие розетки в обычае 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, наконец, добавил Раскадровка Ссылок, некоторое время назад, что делает его гораздо легче.)
вот типичная раскадровка с видом контейнера везде. Все это вид контейнера. Это просто, как вы делаете приложения.
(как любопытство, ответ Кенка показывает точно, как это было сделано, чтобы загрузить xib в вид оболочки, так как вы не можете действительно "назначить себе".)
я добавляю это как отдельный пост, чтобы обновить ситуацию с выпуском Swift. Подход, описанный Леонатаном, отлично работает в 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 } }недостатком этого подхода является введение дополнительного избыточного слоя в иерархию представлений, которого не существует при использовании подхода, описанного Леонатаном в Objective-C. Однако это может быть принято как необходимое зло и продукт фундаментального способа проектирования в Xcode (мне все еще кажется сумасшедшим, что так трудно связать a пользовательский класс UIView с макетом пользовательского интерфейса таким образом, чтобы он работал последовательно как над раскадровками, так и из кода) – замена
selfОптовая торговля в инициализаторе раньше никогда не казалась особенно интерпретируемым способом делать вещи, хотя наличие по существу двух классов представления на представление тоже не кажется таким уж большим.тем не менее, одним из счастливых результатов этого подхода является то, что нам больше не нужно устанавливать пользовательский класс представления в наш файл класса в interface builder, чтобы обеспечить правильность поведение при назначении
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. У вас есть несколько способов сделать это:a) используйте private
@propertyкоторый установлен только в NIB.@interface MyCustomView : UIView @property (assign, nonatomic) BOOL xib @endи установить "определенные пользователем атрибуты во время выполнения" только в MyCustomView.xib'.
плюсы:
- нет
плюсы:
- просто не работает:
setXib:будет называться послеawakeAfterUsingCoder:b) проверьте, если
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]; } }плюсы:
- нет трюк в Interface Builder.
плюсы:
- вы не можете иметь подвиды в раскадровке.
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 @endMyCustomView.м:
#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плюсы:
- нет явного
ifinMyCustomViewплюсы:
- начинаются
_NIB_трюк в xib Interface Builder- относительно больше кодов
e) использовать подкласс в качестве заполнителя в раскадровке
аналогично
d)но использовать подкласс в раскадровке, оригинал класс в 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который декодируется экземпляр F раскадровки.frameи Autolayout/свойства автоматического изменения размеров особенно важны.- (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; } @endXIB:
раскадровка:
результат:
Не забудьте
два важных момента:
- установите владельца файла .xib имя пользовательского класса представления.
- не установите имя пользовательского класса В IB для .корневой вид xib.
Я несколько раз заходил на эту страницу вопросов и ответов, учась создавать многоразовые представления. Забывая вышеизложенные моменты, я потратил много времени, пытаясь выяснить, что вызывает бесконечную рекурсию. Эти пункты являются упоминается в других ответах здесь и в другом месте, но я просто хочу еще раз подчеркнуть их здесь.
мой полный быстрый ответ с шагами здесь.
есть решение, которое намного чище, чем выше решений : https://www.youtube.com/watch?v=xP7YvdlnHfA
нет свойств времени выполнения, нет рекурсивного вызова проблемы вообще. Я попробовал его, и он работал как шарм, используя от раскадровки и от XIB с IBOutlet свойствами (iOS8.1, XCode6).
удачи в кодировании!





Comments