Загрузить представление из внешнего xib-файла в раскадровке


Я хочу использовать представление в нескольких viewcontrollers в раскадровке. Таким образом, я думал о разработке представления во внешнем xib, поэтому изменения отражаются в каждом viewcontroller. Но как можно загрузить представление из внешнего xib в раскадровке и возможно ли это вообще? Если это не так, то какие другие альтернативы доступны, чтобы удовлетворить ситуацию об этом?

8 80

8 ответов:

мой полный пример здесь, но я приведу резюме ниже.

планировка

добавить a .быстрый и. xib файл каждый с тем же именем для вашего проекта. Этот.xib-файл содержит ваш пользовательский макет представления (предпочтительно с использованием ограничений auto layout).

сделайте swift-файл владельцем xib-файла.

enter image description here код

добавьте следующий код .быстрый файл и подключение выходы и действия со стороны .файл xib.

import UIKit
class ResuableCustomView: UIView {

    let nibName = "ReusableCustomView"
    var contentView: UIView?

    @IBOutlet weak var label: UILabel!
    @IBAction func buttonTap(_ sender: UIButton) {
        label.text = "Hi"
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        self.addSubview(view)
        contentView = view
    }

    func loadViewFromNib() -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nibName, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}

использовать

используйте свой пользовательский вид в любом месте раскадровки. Просто добавьте UIView и установите имя класса для вашего пользовательского имени класса.

enter image description here

предполагая, что вы создали xib, который вы хотите использовать:

1) Создайте пользовательский подкласс UIView (вы можете перейти в File -> New -> File... - >Какао Сенсорный Класс. Убедитесь, что" подкласс: "- это"UIView").

2) Добавьте представление, основанное на xib в качестве подвида к этому представлению при инициализации.

В Obj-C

-(id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename"
                                                              owner:self
                                                            options:nil] objectAtIndex:0];
        xibView.frame = self.bounds;
        xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [self addSubview: xibView];
    }
    return self;
}

В Swift 2

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    self.addSubview(xibView)
}

В Swift 3

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    self.addSubview(xibView)
}

3) где вы хотите использовать его в своей раскадровка, добавьте UIView, как обычно, выберите недавно добавленное представление, перейдите к инспектору идентификации (третий значок в правом верхнем углу, который выглядит как прямоугольник с линиями в нем) и введите имя вашего подкласса в качестве "класса" в разделе "пользовательский класс".

пока подход Кристофера Свейси был лучший подход, который я нашел. Я попросила двух старших разработчиков в моей команде об этом и один из них был идеальным решением! Он удовлетворяет все проблемы, которые Кристофер Свейси так красноречиво рассматривал, и он не требует шаблонного кода подкласса(моя главная забота о его подходе). Есть один попался, но кроме этого он довольно интуитивно понятен и прост в реализации.

  1. создайте пользовательский класс UIView в a .swift-файл для управления вашим xib. то есть MyCustomClass.swift
  2. создать .xib файл и стиль его, как вы хотите. то есть MyCustomClass.xib
  3. установить File's Owner часть .xib файл будет вашим пользовательским классом (MyCustomClass)
  4. попался: оставить class значение (под identity Inspector) для представления в .xib файл пустой. таким образом, ваш пользовательский вид не будет иметь определенного класса, но он будет иметь указан владелец файла.
  5. подключите свои розетки, как вы обычно используете Assistant Editor.
    • Примечание: Если вы посмотрите на Connections Inspector вы заметите, что ваши ссылки на СМИ не ссылаться на ваш пользовательский класс (т. е. MyCustomClass), а ссылка File's Owner. Так как File's Owner определяется как ваш пользовательский класс, розетки будут подключаться и работать должным образом.
  6. убедитесь, что ваш пользовательский класс имеет @IBDesignable перед классом заявление.
  7. сделайте свой пользовательский класс соответствующим NibLoadable протокол, указанный ниже.
    • Примечание: Если ваш пользовательский класс .swift имя файла отличается от вашего .xib имя файла, а затем выберите nibName свойство должно быть именем вашего .
  8. реализовать required init?(coder aDecoder: NSCoder) и override init(frame: CGRect) называть setupFromNib() как в примере ниже.
  9. добавить UIView к нужной раскадровке и установить класс, чтобы быть вашим обычаем имя класса (т. е. MyCustomClass).
  10. смотреть IBDesignable в действии, как он рисует ваш .xib в раскадровке со всем этим, страх и удивление.

вот протокол, на который вы хотите ссылаться:

public protocol NibLoadable {
    static var nibName: String { get }
}

public extension NibLoadable where Self: UIView {

    public static var nibName: String {
        return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
    }

    public static var nib: UINib {
        let bundle = Bundle(for: Self.self)
        return UINib(nibName: Self.nibName, bundle: bundle)
    }

    func setupFromNib() {
        guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
        view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
        view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
        view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
    }
}

а вот пример MyCustomClass, который реализует протокол (с указанием .xib файл с именем MyCustomClass.xib):

@IBDesignable
class MyCustomClass: UIView, NibLoadable {

    @IBOutlet weak var myLabel: UILabel!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupFromNib()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupFromNib()
    }

}

Примечание: Если вы пропустите Gotcha и установите class значение внутри вашего .xib-файл будет вашим пользовательским классом, затем он не будет рисовать в раскадровке, и вы получите EXC_BAD_ACCESS ошибка при запуске приложения, потому что он застревает в бесконечном цикле пытается инициализировать класс из пера с помощью init?(coder aDecoder: NSCoder) метод, который затем называет Self.nib.instantiate и называет init снова.

Я всегда находил решение "добавить его в качестве подвида" неудовлетворительным, видя, как он завинчивается с (1) autolayout, (2) @IBInspectable и (3) розетки. Вместо этого, позвольте мне познакомить вас с магией awakeAfter:, an NSObject метод.

awakeAfter позволяет поменять объект на самом деле проснулся от пера/раскадровки с другим объектом полностью. это объект после этого положен через процесс оводнения, имеет awakeFromNib вызывается на нем, добавляется в виде представления и т. д.

мы можем использовать это в подклассе "картонный вырез" нашего представления, единственной целью которого будет загрузка представления из пера и возврат его для использования в раскадровке. Встраиваемый подкласс затем указывается в инспекторе идентификации представления раскадровки, а не в исходном классе. На самом деле это не обязательно должен быть подкласс, чтобы это работало, но сделать его подклассом-это то, что позволяет IB видеть любые свойства IBInspectable/IBOutlet.

Примечание: класс, установленный в представлении в файле NIB, остается прежним. Вложимый подкласс-это только используется в раскадровке. Подкласс не может быть использован для создания экземпляра представления в коде,поэтому он не должен иметь никакой дополнительной логики. Он должен только содержат awakeAfter крюк.

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
  }
}

⚠️ один существенный недостаток здесь заключается в том, что если вы определяете ограничения ширины, высоты или соотношения сторон в раскадровке, которые не относятся к другому виду, то они имеют для копирования вручную. Ограничения, которые связывают два представления, устанавливаются на ближайшем общем предке, а представления пробуждаются из раскадровки изнутри, поэтому к тому времени, когда эти ограничения гидратируются на супервизоре, своп уже произошел. Ограничения, которые связаны только с рассматриваемым представлением, устанавливаются непосредственно на этом представлении и, таким образом, отбрасываются при выполнении подкачки, если они не скопированы.

обратите внимание, что то, что происходит здесь установлены ограничения на вид в раскадровке копируются в новый экземпляр view, который уже может иметь собственные ограничения, определенные в его файле nib. На них это никак не влияет.

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!

    for constraint in constraints {
      if constraint.secondItem != nil {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
      } else {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
      }
    }

    return newView as Any
  }
}  

instantiateViewFromNib является типобезопасным расширением для UIView. Все, что он делает, это цикл через объекты пера, пока он не найдет тот, который соответствует типу. Обратите внимание, что общий тип-это возвращение значение, поэтому тип должен быть указан при вызове сайт.

extension UIView {
  public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
    if let objects = bundle.loadNibNamed(nibName, owner: nil) {
      for object in objects {
        if let object = object as? T {
          return object
        }
      }
    }

    return nil
  }
}

лучшее решение в настоящее время-просто использовать пользовательский контроллер представления с его представлением, определенным в xib, и просто удалить свойство "view", которое xcode создает внутри раскадровки при добавлении к нему vc ( не забудьте установить имя пользовательского класса).

Это заставит среду выполнения автоматически искать xib и загружать его. Вы можете использовать этот трюк для любого вида представлений контейнера или представления содержимого.

Я думаю о alternative использовать XIB views использовать View Controllerв отдельной раскадровки.

затем в основной раскадровке вместо пользовательского вида используйте container view С Embed Segue и StoryboardReference этой настраиваемое представление-контроллер какой вид должен быть помещен внутри другого вида в основной раскадровке.

тут можно настройка делегирования и связь между этим встроенным ViewController и контроллером основного вида через подготовка к segue. Этот подход разные затем отображение UIView, но гораздо проще и эффективнее (с точки зрения программирования) может быть использовано для достижения той же цели, т. е. иметь многоразовый пользовательский вид, который виден в основной раскадровке

дополнительным преимуществом является то, что вы можете реализовать свою логику в классе CustomViewController и там настроить все делегирование и просмотр подготовки без создания отдельных (сложнее найти в проекте) классы контроллера, и без размещения шаблонного кода в главном UIViewController с помощью компонента. Я думаю, что это хорошо для многоразовых компонентов ex. Компонент музыкального проигрывателя (виджет, подобный), который можно встраивать в другие представления.

Это решение может быть использовано, даже если ваш класс не имеет того же имени, что и XIB. Например, если у вас есть базовый класс контроллера вида controllerA, который имеет имя xib controllerA.xib и вы подкласс этого с controllerB и хотите создать экземпляр controllerB в раскадровке, то вы можете:

  • создать представление-контроллер в раскадровке
  • установите класс контроллера в controllerB
  • удалить вид controllerB в раскадровке
  • переопределить вид нагрузки в controllerA:

*

- (void) loadView    
{
        //according to the documentation, if a nibName was passed in initWithNibName or
        //this controller was created from a storyboard (and the controller has a view), then nibname will be set
        //else it will be nil
        if (self.nibName)
        {
            //a nib was specified, respect that
            [super loadView];
        }
        else
        {
            //if no nib name, first try a nib which would have the same name as the class
            //if that fails, force to load from the base class nib
            //this is convenient for including a subclass of this controller
            //in a storyboard
            NSString *className = NSStringFromClass([self class]);
            NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
            UINib *nib ;
            if (pathToNIB)
            {
                nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
            }
            else
            {
                //force to load from nib so that all subclass will have the correct xib
                //this is convenient for including a subclass
                //in a storyboard
                nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
            }

            self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
       }
}

решение для Objective-C в соответствии с шагами, описанными в ответ Бена патча.

использовать расширение для UIView:

@implementation UIView (NibLoadable)

- (UIView*)loadFromNib
{
    UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
    xibView.translatesAutoresizingMaskIntoConstraints = NO;
    [self addSubview:xibView];
    [xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
    [xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
    [xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
    [xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
    return xibView;
}

@end

создать файлы MyView.h,MyView.m и MyView.xib.

сначала подготовить свой MyView.xib как ответ Бена патча так говорит set class MyView для владельца файла вместо основного вида внутри этого XIB.

MyView.h:

#import <UIKit/UIKit.h>

IB_DESIGNABLE @interface MyView : UIView

@property (nonatomic, weak) IBOutlet UIView* someSubview;

@end

MyView.m:

#import "MyView.h"
#import "UIView+NibLoadable.h"

@implementation MyView

#pragma mark - Initializers

- (id)init
{
    self = [super init];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self loadFromNib];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self internalInit];
}

- (void)internalInit
{
    // Custom initialization.
}

@end

а позже просто создайте свой вид программно:

MyView* view = [[MyView alloc] init];

предупреждение! предварительный просмотр этого представления не будет отображаться в раскадровке, если вы используете расширение WatchKit из-за этой ошибки в Xcode >= 9.2: https://forums.developer.apple.com/thread/95616