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

131

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

Себастьян Хоффманн
источник

Ответы:

133

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

раскладка

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

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

введите описание изображения здесь Код

Добавьте следующий код в файл .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и установите имя класса для своего собственного имени класса.

введите описание изображения здесь

Suragch
источник
3
Разве loadNibNamed не вызывает init (coder :)? У меня сбой при попытке адаптировать ваш подход.
Fishman
@Fishman, если вы попытаетесь загрузить представление программно (а не из раскадровки), оно выйдет из строя, потому что в настоящее время у него нет файла init(frame:). См. Это руководство для получения более подробной информации.
Suragch
7
Другая распространенная причина сбоя - это не установка пользовательского представления для владельца файла . Смотрите красный кружок в моем ответе.
Suragch
5
Да, я установил класс корневого представления вместо владельца файла, и это вызвало бесконечный цикл.
devios1
2
После добавления строки view.autoresizingMask = [.f flexibleWidth, .f flexibleHeight] перед self.addSubview (view) он работает отлично.
Прамод Тапания
69

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

  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()
    }

}

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

Бен Патч
источник
2
Вот еще один отличный подход к этому, но я считаю, что приведенный выше все же лучше: medium.com/zenchef-tech-and-product/…
Бен Патч
4
Подход, упомянутый вами выше, отлично работает и применяет предварительный просмотр прямо в раскадровке. Это очень удобно и круто!
Игорь Леонович
1
К вашему сведению: это решение, использующее определение ограничения в setupFromNib(), похоже, исправляет некоторые странные проблемы с автоматической компоновкой с ячейками представления таблицы с автоматическим изменением размера, содержащими представления, созданные XIB.
Гэри
1
Однозначно лучшее решение! Люблю @IBDesignableсовместимость. Не могу поверить, почему Xcode или UIKit не предоставляют что-то подобное по умолчанию при добавлении файла UIView.
fl034
1
Кажется, это не работает ... Каждый раз, когда я устанавливаю владельца файла в моем .xib файле, он ТАКЖЕ устанавливает собственный класс.
drewster
32

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

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

user1021430
источник
xibView.frame = self.frame;должно быть xibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);, иначе xibView будет иметь смещение при добавлении представления в раскадровку.
BabyPanda 01
опоздал на вечеринку, но, похоже, он изменил его на xibView.frame = self.bounds, который представляет собой фрейм без смещения
Heavy_Bullets
20
Приводит к сбою из-за бесконечной рекурсии. Загрузка пера создает еще один экземпляр подкласса.
Дэвид
2
Класс представления xib не должен совпадать с этим новым подклассом. Если xib - это MyClass, вы можете сделать этот новый класс MyClassContainer.
user1021430
приведенное выше решение приведет к сбою до бесконечной рекурсии.
JBarros35,
28

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

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

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

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

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

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
  }
}
Кристофер Сваси
источник
Это ошеломляет. Если я не ошибаюсь, это работает только «на раскадровке» - если вы попытаетесь создать такой класс в коде во время выполнения, я не думаю, что это сработает. Я считаю.
Fattie
Подкласс должен работать в коде так же хорошо, как и исходный класс для всех намерений и целей. Если вы хотите загрузить представление из пера в коде, вы должны просто создать его экземпляр напрямую, используя ту же технику. Все, что делает подкласс, - это берет код для создания экземпляра представления из пера и помещает его в ловушку для использования раскадровкой.
Кристофер
На самом деле я был неправ - это сработало бы так же хорошо, если бы вы могли его создать, но вы не можете, потому что представление в NIB будет иметь суперкласс в качестве своего типа, поэтому instantiateViewFromNibничего не вернет. В любом случае это не имеет большого значения, ИМО, подкласс - это просто приспособление для подключения к раскадровке, весь код должен быть в исходном классе.
Кристофер
1
Большой! Одна вещь сбила меня с толку, потому что у меня мало опыта работы с xibs (я когда-либо работал только с раскадровками и программными подходами), оставив это здесь на случай, если это кому-то поможет: в файле .xib вам нужно выбрать представление верхнего уровня и установить его тип класса MyCustomView. В моем xib левая внутренняя боковая панель по умолчанию отсутствовала; Чтобы включить его, есть кнопка рядом с элементом управления чертами «Просмотреть как: iPhone 7» в нижней / левой части.
xaphod
5
Он тормозит ограничения при замене другим объектом. :(
invoodoo 05
6

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

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

Бен Джи
источник
5

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

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

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

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

Михал Зиобро
источник
На самом деле, к сожалению, гораздо более простое решение, чем использование пользовательского представления xib :(
Уилсон
1
после того, как я пошел по кругу с цепочкой создания жизненного цикла Apple xib в пользовательском UIView, я так и закончил.
LightningStryk
В случае, если вы хотите добавить настраиваемое представление динамически, нам все равно придется использовать отдельный контроллер представления (наиболее предпочтительно XIB)
Сатьям,
5

Хотя самые популярные ответы работают нормально, концептуально они неверны. Все они используются в File's ownerкачестве связи между выходами класса и компонентами пользовательского интерфейса. File's ownerпредполагается использовать только для объектов верхнего уровня, а не UIViews. Ознакомьтесь с документом разработчика Apple . Наличие UIView as File's ownerприводит к этим нежелательным последствиям.

  1. Вы вынуждены использовать contentViewтам, где должны использовать self. Это не только некрасиво, но и структурно неверно, потому что промежуточное представление не позволяет структуре данных передавать структуру пользовательского интерфейса. Это как противоположность декларативного пользовательского интерфейса.
  2. У вас может быть только один UIView на Xib. Предполагается, что Xib имеет несколько UIView.

Есть элегантный способ сделать это без использования File's owner. Пожалуйста, проверьте это сообщение в блоге . В нем объясняется, как это делать правильно.

Ingun 전인 건
источник
совершенно неважно, ты не объяснил, почему это плохо. Я не понимаю почему. побочных эффектов нет.
JBarros35,
1

Вот ответ, который вы хотели все это время. Вы можете просто создать свой 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

Том 👣

barefeettom
источник
Отличное решение, спасибо!
аве
Я не хочу устанавливать фреймворк, когда он должен быть предоставлен изначально.
JBarros35,
0

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

  • создать контроллер представления в раскадровке
  • установить класс контроллера на controllerB
  • удалить представление контроллера 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];
       }
}
otusweb
источник
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как ответ Бен Патча так говорит набор классов 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

Ариэль Богдзевич
источник
Должен был предоставить версию Swift. Очень немногие могут использовать ObjC в настоящее время
Сатьям,