Загрузить представление из внешнего файла XIB в раскадровке - PullRequest
108 голосов
/ 14 февраля 2012

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

Ответы [ 9 ]

100 голосов
/ 30 декабря 2015

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

Компоновка

Добавьте в свой проект файлы .swift и .xib, каждый с одинаковым именем. Файл .xib содержит ваш пользовательский макет представления (предпочтительно с использованием ограничений автоматического макета).

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

enter image description here код

Добавьте следующий код в файл .swift и подключите выходы и действия из файла .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

55 голосов
/ 15 ноября 2017

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

  1. Создайте пользовательский класс UIView в файле .swift для управления вашим xib.т.е. MyCustomClass.swift
  2. Создайте файл .xib и стилизуйте его так, как вам хочется.т.е. MyCustomClass.xib
  3. Установите File's Owner файла .xib в качестве пользовательского класса (MyCustomClass)
  4. GOTCHA: оставьте значение class(под identity Inspector) для пользовательского представления в пустом файле .xib. Таким образом, в вашем пользовательском представлении не будет указанного класса, но у него будет указанный владелец файла.
  5. Подключите розетки, как обычно, используя Assistant Editor.
    • ПРИМЕЧАНИЕ. Если вы посмотрите на Connections Inspector, вы заметите, что ваши ссылочные выходы не ссылаются на ваш пользовательский класс (т.е. MyCustomClass), а скорее ссылаются на File's Owner.Поскольку File's Owner определено как ваш пользовательский класс, розетки будут подключены и будут работать правильно.
  6. Убедитесь, что ваш пользовательский класс имеет @IBDesignable перед оператором класса.
  7. Сделайте ваш пользовательский класс соответствующим протоколу NibLoadable, указанному ниже.
    • ПРИМЕЧАНИЕ. Если имя файла пользовательского класса .swift отличается от имени файла .xib, задайте для свойства nibName имя файла .xib.
  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.

30 голосов
/ 19 января 2016

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

1) Создайте пользовательский подкласс UIView (вы можете перейти в File -> New -> File ... -> Cocoa TouchКласс. Убедитесь, что «Подкласс:» равен «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, как выкак правило, выберите вновь добавленное представление, перейдите в Инспектор идентификации (третий значок в правом верхнем углу, который выглядит как прямоугольник с линиями в нем) и введите имя вашего подкласса в качестве «Класса» в разделе «Пользовательский класс».

26 голосов
/ 31 октября 2016

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

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

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

Этот дополнительный шаблон может показаться неоптимальным - и в некотором смысле это так, потому что в идеале UIStoryboard справился бы с этим беспрепятственно - но у него есть преимущество, заключающееся в том, что исходный NIB и подкласс UIView полностью не изменяются. Роль, которую он играет, - это, в основном, класс адаптера или моста, и он совершенно допустим с точки зрения дизайна в качестве дополнительного класса, даже если он вызывает сожаление. С другой стороны, если вы предпочитаете экономить на своих классах, решение @ BenPatch работает путем реализации протокола с некоторыми другими незначительными изменениями. Вопрос о том, какое решение лучше, сводится к стилю программиста: предпочитаете ли вы композицию объектов или множественное наследование.

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

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

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

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

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. Все, что он делает, это перебирает объекты NIB, пока не найдет тот, который соответствует типу. Обратите внимание, что универсальным типом является значение return , поэтому тип должен быть указан на сайте вызова.

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
  }
}
5 голосов
/ 06 марта 2018

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

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

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

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

5 голосов
/ 11 марта 2015

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

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

0 голосов
/ 09 марта 2019

Вот ответ, который вы хотели с самого начала.Вы можете просто создать свой класс CustomView, иметь его основной экземпляр в xib со всеми подпредставлениями и выходами.Затем вы можете применить этот класс к любым экземплярам в ваших раскадровках или других XIB-файлах.

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

Просто сделайте это:

  1. Импортируйте структуру BFWControls
  2. Измените ваш суперкласс с UIView на NibView (илиот UITableViewCell до NibTableViewCell)

Вот и все!

Он даже работает с IBDesignable для ссылки на ваш пользовательский вид (включая подпредставления из xib) во время разработки вРаскадровка.

Подробнее об этом можно прочитать здесь: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

И вы можете получить фреймворк с открытым исходным кодом BFWControls здесь: https://github.com/BareFeetWare/BFWControls

А вот простая выдержкаNibReplaceable кода, который управляет им, на случай, если вам интересно: https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

Том 32

0 голосов
/ 02 августа 2018

Решение для 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 как Ответ Бен Патча говорит, что задайте класс 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

0 голосов
/ 08 апреля 2015

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

  • создать контроллер представления в раскадровке
  • установить класс контроллера на контроллерB
  • удалить представление контроллера B в раскадровке
  • переопределить представление нагрузки в контроллере A на:

*

- (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];
       }
}
...