如何创建自定义iOS视图类并实例化它的多个副本(在IB中)?


114

我目前正在制作一个具有多个计时器的应用程序,这些计时器基本上都是相同的。

我想创建一个自定义类,该类使用计时器的所有代码以及布局/动画,因此我可以拥有5个相同的计时器,它们彼此独立运行。

我使用IB(xcode 4.2)创建了布局,计时器的所有代码当前都位于viewcontroller类中。

我很难集中精力如何将所有内容封装到自定义类中,然后将其添加到视图控制器中,我们将不胜感激。


对于任何在这里搜索的人…… 从iOS的容器视图到现在已经五年了!容器视图是您现在如何在iOS中执行所有操作的基本核心思想。他们是令人难以置信的简单易用,并有任意数量的集装箱看法优秀(五岁!)教程左右,例如例如
Fattie

2
@JoeBlow除非您不能在UITableViewCell或中使用容器视图UICollectionViewCell。就我而言,我需要一个很小但相当复杂的视图,可以在控制器视图和集合视图中多次使用。设计师不断地对其进行修改,因此我希望在单个位置定义布局。因此,笔尖。
梯队

Answers:


142

从概念上讲,您的计时器应该是的子类,UIView而不是的子类NSObject

要在IB中实例化计时器的实例,只需将其UIView拖放到视图控制器的视图上,并将其类设置为计时器的类名即可。

在此处输入图片说明

记住#import您的视图控制器中的计时器类。

编辑:用于IB设计(有关代码实例化,请参见修订历史记录)

我对故事板一点都不熟悉,但是我确实知道您可以使用.xib与使用故事板版本几乎相同的文件在IB中构建界面。您甚至应该甚至可以将整个视图从现有界面复制并粘贴到.xib文件中。

为了测试这一点,我创建了一个.xib名为“ MyCustomTimerView.xib” 的新空文件。然后,我添加了一个视图,并在其中添加了标签和两个按钮。像这样:

在此处输入图片说明

我创建了一个UIView名为“ MyCustomTimer” 的新的Objective-C类子类。在我中,.xib我将文件的Owner类设置为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:确实像它听起来那样。它加载Nib并将“文件的所有者”属性设置为self。我们提取数组中的第一个对象,这就是Nib的根视图。我们将视图添加为子视图,然后将其显示在屏幕上。

显然,这只是在按下按钮时更改标签的背景颜色,但是此示例可以使您顺利进行。

基于注释的注释:

值得注意的是,如果遇到无限递归问题,您可能会错过此解决方案的巧妙技巧。它没有按照您认为的去做。看不到放置在情节提要中的视图,而是将另一个视图加载为子视图。它加载的视图是在笔尖中定义的视图。笔尖中的“文件所有者”是那种看不见的视图。最酷的部分是,这种看不见的视图仍然是Objective-C类,可以用作从笔尖引入的视图的各种视图控制器。例如,类中的IBAction方法在MyCustomTimer视图控制器中比在视图中期望更多。

附带说明一下,有些人可能会争辩说这打破了MVC,我有些同意。从我的角度来看,它与custom紧密相关,custom UITableViewCell有时也必须是部分控制器。

还值得注意的是,这个答案是提供一个非常具体的解决方案。创建一个可以在情节提要上布置的同一视图上多次实例化的笔尖。例如,您可以轻松地一次在iPad屏幕上想象其中的六个计时器。如果您只需要为视图控制器指定一个视图,该视图控制器将在您的应用程序中多次使用,那么jyavenard为这个问题提供的解决方案几乎可以肯定是您更好的解决方案。


这实际上似乎是一个单独的问题。但是我认为可以很快得到答复,所以就到这里。每次创建新通知时,它都是一个新通知,但是如果您需要它们具有相同的名称,则想添加一些内容来区分它们,则可以使用userInfo通知中的属性@property(nonatomic,copy) NSDictionary *userInfo;
NJones 2012年

28
我从initWithCoder获得了无限递归。有任何想法吗?
消失的

6
对不起,要恢复旧线程,请确保File's Owner将其设置为您的类名,这解决了我的无限递归问题。
themanatuf

20
无限递归的另一个原因是将xib中的根视图设置为您的自定义类。您不想这样做,请将其保留为默认的“ UIView”
2013年

11
关于此方法,只有一件事引起了我的注意……现在2个视图都在同一个类中,这是否会使任何人感到困扰?从UIView继承的原始文件.h / .m成为第一个文件,并通过 [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; 添加第二个UIView ...对我来说这有点奇怪,没有其他人会有同样的感觉吗?如果是这样,这是苹果设计制造的吗?还是通过这样做找到了解决方案?
2013年

137

迅捷的例子

已针对Xcode 10和Swift 4更新

这是一个基本的演练。最初,我通过观看Youtube视频系列中学到了很多。后来我根据本文更新了答案。

添加自定义视图文件

以下两个文件将构成您的自定义视图:

  • .xib文件包含布局
  • .swift文件作为UIView子类

添加它们的详细信息如下。

Xib文件

将.xib文件添加到您的项目(文件>新建>文件...>用户界面>视图)。我打电话给我ReusableCustomView.xib

创建您希望自定义视图具有的布局。例如,我将使用UILabel和进行布局UIButton。使用自动布局是个好主意,这样无论您以后将其设置为什么大小,东西都会自动调整大小。(我在“属性”检查器中将Freeform用于xib大小,以便可以调整模拟量度,但这不是必需的。)

在此处输入图片说明

迅捷文件

将.swift文件添加到您的项目(“文件”>“新建”>“文件...”>“源”>“ Swift文件”)。它是的子类,UIView我称之为我的ReusableCustomView.swift

import UIKit
class ResuableCustomView: UIView {

}

将Swift文件设为所有者

返回您的.xib文件,然后在“文档大纲”中单击“文件的所有者”。在身份检查器中,将.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在“身份检查器”中将视图的类名称设置为。

在此处输入图片说明


31
重要的是不要将Xib视图类设置为ResuableCustomView,而是此Xib的所有者。否则,您将有错误。
RealNmae

5
是的,很好的提醒。我不知道我花了多少时间来尝试解决由于将Xib的视图(而不是File Owner)设置为类名而导致的无限递归问题。
Suragch '16

2
@TomCalmon,我用Swift 3在Xcode 8中重做了这个项目,“自动布局”对我来说工作正常。
Suragch

6
如果其他人对IB中的视图呈现有疑问,我发现更改Bundle.mainBundle(for: ...)可以解决问题。显然,Bundle.main在设计时不可用。
crizzis '16

4
这不适用于@IBDesignable的原因是您没有实现init(frame:)。一旦添加(并将所有初始化代码放入某个commonInit()函数中),它就可以正常工作并显示在IB中。在这里看到更多的信息:stackoverflow.com/questions/31265906/...
季米特里斯

39

回答视图控制器,而不是视图

有一种更简单的方法可以从情节提要中加载xib。假设您的控制器属于MyClassController类型,且继承自UIViewController

UIViewController在情节提要中添加了使用IB;将类类型更改为MyClassController。删除已自动添加到情节提要中的视图。

确保要调用的XIB名为MyClassController.xib。

在故事板加载期间实例化该类时,将自动加载xib。这样做的原因是由于默认实现UIViewController会调用以类名命名的XIB。


1
我一直在给自定义视图充气,但从来不知道将它加载到情节提要中的小技巧。非常感激!
MrTristan

38
问题是关于视图,而不是关于视图控制器。
里卡多

为澄清
起见

1
在iOS 9.1中似乎无法正常工作。如果删除自动创建的(默认)视图,则从VC的viewDidLoad返回后,它将崩溃。我认为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

这并不是真正的答案,但我认为分享这种方法会有所帮助。

目标C

  1. CustomViewWithXib.hCustomViewWithXib.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)
    }
}

您可以在此处找到一些示例。

希望有帮助。


那么我们如何为视图uicontrols添加iboutlet
Amr Angry

1
嘿@AmrAngry,要连接视图,您应该执行4步:转到您的xib文件,在“文件所有者”中设置您的类,然后通过点击“ ctrl”将您的视图拖放到界面上,我更新了该示例的代码源,如果有任何疑问,请不要犹豫,希望能对您有所帮助
HamzaGhazouani

非常感谢您的重放,我已经做到了,但是我想我的问题不在这一部分。我的问题是我添加了一个UIDatePIcker并尝试从ViewController设置此控件的目标操作,并设置了uiControleer事件的值,但该方法从未调用/触发
Amr Angry

1
@AmrAngry我通过将选择器视图更新例如,可能是它会帮助你:) github.com/HamzaGhazouani/Stackoverflow/tree/master/...
HamzaGhazouani

1
谢谢,我一直在寻找这个很长一段时间。
MH175
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.