Как создать собственный класс представления iOS и создать несколько его копий (в IB)?

114

В настоящее время я делаю приложение, которое будет иметь несколько таймеров, которые в основном одинаковы.

Я хочу создать собственный класс, который использует весь код для таймеров, а также макет / анимацию, чтобы у меня было 5 одинаковых таймеров, которые работают независимо друг от друга.

Я создал макет с помощью IB (xcode 4.2), и весь код для таймеров в настоящее время находится только в классе viewcontroller.

Мне трудно понять, как инкапсулировать все в пользовательский класс, а затем добавить его в viewcontroller, любая помощь будет очень признательна.

Майкл Кэмпсолл
источник
Для всех, кто ищет здесь в Google ... Прошло пять лет с того, как представления контейнеров появятся в iOS! Представления контейнеров - это основная, центральная идея того, как вы сейчас все делаете в iOS. Они невероятно просты в использовании, и существует множество отличных (пятилетних!) Руководств по представлениям контейнеров, например , например
Fattie
2
@JoeBlow За исключением того, что вы не можете использовать представление контейнера в UITableViewCellили UICollectionViewCell. В моем случае мне нужно небольшое, но довольно сложное представление, которое я могу использовать много раз в контроллерах и представлениях коллекций. И дизайнеры продолжают его переделывать, поэтому я хочу, чтобы макет был определен в одном месте. Следовательно, перо.
Echelon

Ответы:

142

Чтобы ответить концептуально, ваш таймер, скорее всего, должен быть подклассом UIViewвместо NSObject.

Чтобы создать экземпляр вашего таймера в IB, просто перетащите UIViewего на представление вашего контроллера представления и установите его class равным имени вашего класса таймера.

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

Не забудьте указать #importсвой класс таймера в контроллере представления.

Изменить: для дизайна IB (для создания экземпляра кода см. Историю изменений)

Я совсем не знаком с раскадровкой, но я знаю, что вы можете создать свой интерфейс в IB, используя .xibфайл, который почти идентичен использованию версии раскадровки; Вы даже должны иметь возможность копировать и вставлять свои представления целиком из существующего интерфейса в .xibфайл.

Чтобы проверить это, я создал новый пустой .xibфайл с именем «MyCustomTimerView.xib». Затем я добавил представление, к которому добавил метку и две кнопки. Вот так:

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

Я создал новый подкласс класса объектного C под UIViewназванием «MyCustomTimer». В моем .xibя установил для своего класса владельца файла значение MyCustomTimer . Теперь я могу подключать действия и выходы, как и любой другой вид / контроллер. Полученный .hфайл выглядит так:

@interface MyCustomTimer : UIView
@property (strong, nonatomic) IBOutlet UILabel *displayLabel;
@property (strong, nonatomic) IBOutlet UIButton *startButton;
@property (strong, nonatomic) IBOutlet UIButton *stopButton;
- (IBAction)startButtonPush:(id)sender;
- (IBAction)stopButtonPush:(id)sender;
@end

Единственное препятствие, которое нужно преодолеть, - это получить это .xibна моем UIViewподклассе. Использование .xibзначительно сокращает требуемые настройки. И поскольку вы используете раскадровки для загрузки таймеров, мы знаем, -(id)initWithCoder:что это единственный инициализатор, который будет вызван. Итак, вот как выглядит файл реализации:

#import "MyCustomTimer.h"
@implementation MyCustomTimer
@synthesize displayLabel;
@synthesize startButton;
@synthesize stopButton;
-(id)initWithCoder:(NSCoder *)aDecoder{
    if ((self = [super initWithCoder:aDecoder])){
        [self addSubview:
         [[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" 
                                        owner:self 
                                      options:nil] objectAtIndex:0]];
    }
    return self;
}
- (IBAction)startButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor greenColor];
}
- (IBAction)stopButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor redColor];
}
@end

Названный метод loadNibNamed:owner:options:делает именно то, что кажется. Он загружает Перо и устанавливает для свойства «Владелец файла» значение self. Мы извлекаем первый объект в массиве, который является корневым представлением Nib. Мы добавляем представление как подвид, и вуаля оно отображается на экране.

Очевидно, это просто меняет цвет фона метки при нажатии кнопок, но этот пример должен помочь вам в этом.

Примечания на основе комментариев:

Стоит отметить, что если у вас возникают проблемы с бесконечной рекурсией, вы, вероятно, упустили тонкую уловку этого решения. Он не делает то, что вы думаете. Представление, помещенное в раскадровку, не отображается, а вместо этого загружает другое представление как подпредставление. Это представление, которое он загружает, определено в пере. «Владелец файла» в пере - это невидимое изображение. Крутая часть заключается в том, что это невидимое представление по-прежнему является классом Objective-C, который можно использовать как своего рода контроллер представления для представления, которое он приносит из пера. Например, IBActionметоды в MyCustomTimerклассе - это то, чего вы ожидаете больше от контроллера представления, чем от представления.

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

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

NJones
источник
На самом деле это отдельный вопрос. Но я думаю, что на него можно быстро ответить, так что приступим. Каждый раз, когда вы создаете новое уведомление, это новое уведомление, но если вам нужно, чтобы у них было одно и то же имя, вы хотите добавить что-то, чтобы различать их, используйте userInfoсвойство в уведомлении@property(nonatomic,copy) NSDictionary *userInfo;
NJones,
28
Я получаю бесконечную рекурсию от initWithCoder. Любые идеи?
исчез
6
Извините за возрождение старого потока, убедившись, что File's Ownerустановлено имя вашего класса, моя проблема с бесконечной рекурсией решена.
themanatuf
20
Другой причиной бесконечной рекурсии является установка корневого представления в xib на ваш собственный класс. Если вы не хотите этого делать, оставьте значение по умолчанию «UIView»
XY
11
Есть только одна вещь, которая привлекает мое внимание в этом подходе ... Никого не беспокоит, что 2 представления теперь находятся в одном классе? Исходный файл .h / .m, унаследованный от UIView, являющийся первым, и путем [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; добавления второго UIView ... Это выглядит странно для меня, никто другой не чувствует того же? Если да, то это яблоко сделано по дизайну? Или решение кто-то нашел, сделав это?
SH
137

Быстрый пример

Обновлено для Xcode 10 и Swift 4

Вот базовая процедура. Изначально я многому научился из этого сериала видео на Youtube . Позже я обновил свой ответ на основе этой статьи .

Добавить файлы пользовательского просмотра

Следующие два файла сформируют ваше собственное представление:

  • .xib файл, содержащий макет
  • .swift файл как UIViewподкласс

Детали для их добавления приведены ниже.

Xib файл

Добавьте в свой проект файл .xib (Файл> Создать> Файл ...> Пользовательский интерфейс> Просмотр) . Я звоню своим ReusableCustomView.xib.

Создайте макет, который должен иметь ваш пользовательский вид. В качестве примера я сделаю макет с a UILabelи a UIButton. Рекомендуется использовать автоматический макет, чтобы размер объектов автоматически изменялся независимо от того, какой размер вы установите позже. (Я использовал Freeform для размера xib в инспекторе атрибутов, чтобы я мог настроить моделируемые метрики, но в этом нет необходимости.)

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

Swift файл

Добавьте в проект файл .swift (Файл> Создать> Файл ...> Источник> Файл Swift) . Это подкласс, UIViewи я называю его своим ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Сделайте файл Swift владельцем

Вернитесь к вашему файлу .xib и щелкните «Владелец файла» в структуре документа. В Identity Inspector напишите имя вашего файла .swift в качестве имени настраиваемого класса.

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

Добавить собственный код просмотра

Замените ReusableCustomView.swiftсодержимое файла следующим кодом:

import UIKit

@IBDesignable
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)
        commonInit()
    }

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

    func commonInit() {
        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
    }
}

Убедитесь, что вы правильно написали имя вашего файла .xib.

Подключите розетки и действия

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

Используйте свой собственный вид

Ваш индивидуальный вид готов. Все, что вам нужно сделать, это добавить UIViewв главную раскадровку в любом месте. Установите имя класса представления ReusableCustomViewв Identity Inspector.

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

Suragch
источник
31
Важно не устанавливать класс представления Xib как ResuableCustomView, а как Владелец этого Xib. В противном случае у вас будет ошибка.
RealNmae 05
5
Да, хорошее напоминание. Я не знаю, сколько времени я потратил, пытаясь решить проблемы с бесконечной рекурсией, вызванные установкой представления Xib (а не владельца файла) на имя класса.
Suragch 06
2
@TomCalmon, я переделал этот проект в Xcode 8 с Swift 3, и Auto Layout у меня работал нормально.
Suragch
6
Если у кого-то еще есть проблемы с рендерингом представления в IB, я обнаружил, что изменение Bundle.mainна Bundle(for: ...)помогает. Судя по всему, Bundle.mainво время разработки недоступен.
criszzis
4
Причина, по которой это не работает с @IBDesignable, заключается в том, что вы не реализовали init(frame:). После того, как вы добавите это (и поместите весь код инициализации в какую-либо commonInit()функцию), он будет работать нормально и отображаться в IB. См. Дополнительную информацию здесь: stackoverflow.com/questions/31265906/…
Димитрис
39

Ответ для контроллеров представлений, а не представлений :

Есть более простой способ загрузить xib из раскадровки. Скажем, ваш контроллер имеет тип MyClassController, который наследуется от UIViewController.

Вы добавляете UIViewControllerиспользующий IB в свою раскадровку; измените тип класса на MyClassController. Удалите вид, который был автоматически добавлен в раскадровку.

Убедитесь, что XIB, который вы хотите вызвать, называется MyClassController.xib.

Когда будет создан экземпляр класса во время загрузки раскадровки, автоматически загрузится xib. Причина этого связана с реализацией по умолчанию, UIViewControllerкоторая вызывает XIB с именем класса.

jyavenard
источник
1
Я постоянно раздуваю пользовательские представления, но никогда не знал этого маленького трюка, чтобы загрузить его в раскадровку. Очень признателен!
MrTristan
38
Вопрос касается представлений, а не контроллеров представлений.
Ricardo
Добавлен заголовок для уточнения: «Ответ для контроллеров представлений, а не представлений:»
nacho4d 04
1
Кажется, не работает в iOS 9.1. Если я удалю автоматически созданное (по умолчанию) представление, оно выйдет из строя после возврата из viewDidLoad VC. Я не думаю, что представление в XIB подключается вместо представления по умолчанию.
Джефф
1
Я получаю исключение 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Обратите внимание на whacko nibName. Я использую правильное имя класса для пера.
Джефф
16

На самом деле это не ответ, но я думаю, что полезно поделиться этим подходом.

Objective-C

  1. Импортируйте CustomViewWithXib.h и CustomViewWithXib.m в свой проект.
  2. Создайте файлы пользовательского представления с тем же именем (.h / .m / .xib)
  3. Наследуйте свой собственный класс от CustomViewWithXib

стриж

  1. Импортируйте CustomViewWithXib.swift в свой проект
  2. Создайте файлы пользовательского представления с тем же именем (.swift и .xib)
  3. Наследуйте свой собственный класс от CustomViewWithXib

По желанию :

  1. Перейдите в свой xib-файл, установите для владельца свое собственное имя класса, если вам нужно подключить некоторые элементы (более подробную информацию см. В разделе Сделать файл Swift владельцем ответа @Suragch)

Все, теперь вы можете добавить свой собственный вид в свою раскадровку, и он будет показан :)

CustomViewWithXib.h :

 #import <UIKit/UIKit.h>

/**
 *  All classes inherit from CustomViewWithXib should have the same xib file name and class name (.h and .m)
 MyCustomView.h
 MyCustomView.m
 MyCustomView.xib
 */

// This allows seeing how your custom views will appear without building and running your app after each change.
IB_DESIGNABLE
@interface CustomViewWithXib : UIView

@end

CustomViewWithXib.m :

#import "CustomViewWithXib.h"

@implementation CustomViewWithXib

#pragma mark - init methods

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

#pragma mark - setup view

- (UIView *)loadViewFromNib {
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];

    //  An exception will be thrown if the xib file with this class name not found,
    UIView *view = [[bundle loadNibNamed:NSStringFromClass([self class])  owner:self options:nil] firstObject];
    return view;
}

- (void)commonSetup {
    UIView *nibView = [self loadViewFromNib];
    nibView.frame = self.bounds;
    // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
    nibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    // Adding nibView on the top of our view
    [self addSubview:nibView];
}

@end

CustomViewWithXib.swift :

import UIKit

@IBDesignable
class CustomViewWithXib: UIView {

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

        commonSetup()
    }

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

        commonSetup()
    }

    // MARK: setup view 

    private func loadViewFromNib() -> UIView {
        let viewBundle = NSBundle(forClass: self.dynamicType)
        //  An exception will be thrown if the xib file with this class name not found,
        let view = viewBundle.loadNibNamed(String(self.dynamicType), owner: self, options: nil)[0]
        return view as! UIView
    }

    private func commonSetup() {
        let nibView = loadViewFromNib()
        nibView.frame = bounds
        // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
        nibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        // Adding nibView on the top of our view
        addSubview(nibView)
    }
}

Вы можете найти здесь несколько примеров .

Надеюсь, это поможет.

HamzaGhazouani
источник
Итак, как мы можем добавить iboutlet для просмотра uicontrols
Amr Angry
1
Привет, @AmrAngry, для подключения представлений вы должны выполнить 4 шага: перейдите в свой xib-файл, в «Владелец файла» установите свой класс и перетащите ваши представления в интерфейс, нажав сенсорную кнопку «ctrl», я обновляю исходный код с этим примером, не стесняйтесь, если у вас есть какие-либо вопросы, надеюсь, что это поможет
HamzaGhazouani
Большое спасибо за ваш повтор, я уже делаю это, но думаю, моя проблема не в этой части. Моя проблема в том, что я добавляю UIDatePIcker и пытаюсь добавить целевое действие для этого элемента управления из ViewController, чтобы установить, и событие uiControleer изменяет значение, но метод никогда не вызывается / запускается
Amr Angry
1
@AmrAngry Я обновляю пример, добавляя окно выбора, может быть, это поможет вам :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani 01
1
Спасибо Давно искал это.
MH175