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



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




  • нет отдельной UIViewController - полностью автономный класс

  • розетки в классе, чтобы позволить мне установить/получить свойства вид


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





  1. переопределить -(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. переопределить -(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 с минимальной суетой / без тонкой оболочки контроллера? Или есть альтернативный, более чистый способ делать вещи с минимальным шаблонным кодом?

682   6  

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, наконец, добавил Раскадровка Ссылок, некоторое время назад, что делает его гораздо легче.)

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

enter image description here

(как любопытство, ответ Кенка показывает точно, как это было сделано, чтобы загрузить 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, используя выходы/действия и т. д. после установки владельца файла представления на наш пользовательский класс:

Setting the file owner property of the custom view

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

Шаг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
@end

MyCustomView.м:

#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 in MyCustomView

плюсы:

  • начинаются _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;
}
@end

XIB:

XIB screenshot

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

Storyboard

результат:

enter image description here

Не забудьте

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

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

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

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

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

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

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

Comments

    Ничего не найдено.