Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

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

раскладка

Добавьте в ваш проект файлы .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

Ответ 2

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

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

Ответ 3

Предполагая, что вы создали 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, как обычно, выберите новое добавленное представление, перейдите к Identity Inspector (третий значок в правом верхнем углу, который выглядит как прямоугольник с линиями в это) и введите название подкласса в качестве "класса" в разделе "Пользовательский класс".

Ответ 4

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

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

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

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

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

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

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

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

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 расширение 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
  }
}

Ответ 5

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

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

Ответ 6

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

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

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

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

Ответ 7

Это решение можно использовать, даже если ваш класс не имеет то же имя, что и XIB. Например, если у вас есть контроллер контроллера класса BaseA, который имеет XIB-имя контроллера A.xib, и вы подклассифицировали его с помощью контроллера 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];
       }
}

Ответ 8

Решение для Objective-C в соответствии с шагами, описанными в ответе Ben Patch.

Используйте расширение для 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 как говорится в ответе Ben Patch, поэтому установите класс 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

Ответ 9

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

Нет необходимости возиться с владельцем файла, подключать выходы к прокси-серверу, модифицировать 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

Том 👣

Ответ 11

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

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

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