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

122

我目前正在制作一个应用程序,其中将有多个计时器,它们基本上都是相同的。

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

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

我很难理解如何将所有内容封装到自定义类中,然后将其添加到viewcontroller中,任何帮助都将不胜感激。


对于任何正在搜索此处的人...从容器视图进入iOS已经五年了!容器视图是您现在在iOS中执行所有操作的基本中心思想。它们非常简单易用,并且有许多优秀(五年历史!)关于容器视图的教程,例如例如 - Fattie
2
@JoeBlow 除非您不能在 UITableViewCellUICollectionViewCell 中使用容器视图。 在我的情况下,我需要一个小而相当复杂的视图,可以在控制器和集合视图中多次使用。 设计师们不断重新设计它,因此我希望有一个定义布局的单一位置。 因此,需要使用一个nib。 - Echelon
4个回答

152

Swift实例

已更新至Xcode 10和Swift 4(并据报道,仍可用于Xcode 12.4 / Swift 5)

下面是一个基本的步骤指南。我最初从观看这个Youtube视频系列学到了很多内容。后来我根据这篇文章更新了我的答案。

添加自定义视图文件

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

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

它们的添加细节如下。

xib文件

向您的项目中添加.xib文件(文件>新建>文件…>用户界面>视图)。我称之为ReusableCustomView.xib

创建您要自定义视图的布局。例如,我将创建一个带有UILabelUIButton的布局。最好使用自动布局,这样无论您设置多大的尺寸,事物都会自动调整大小。(我在属性检查器中使用了可自由设置的xib大小,以便我可以调整模拟指标,但这不是必需的。)

enter image description here

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布局中的标签和按钮拖动到Swift自定义视图代码,连接出口和动作。

使用您的自定义视图

您的自定义视图现在已完成。您需要在主故事板中想放置它的地方添加一个UIView,并在Identity Inspector中将视图的类名设置为ReusableCustomView

输入图像描述


37
重要的是不要将Xib视图类设置为可重用自定义视图,而是将其所有者设置为Xib。否则会出现错误。 - RealNmae
7
好的提醒。我不知道我浪费了多少时间试图解决由于将Xib的视图(而不是文件拥有者)设置为类名而导致的无限递归问题。 - Suragch
2
@TomCalmon,我用Xcode 8和Swift 3重新做了这个项目,自动布局对我来说工作得很好。 - Suragch
7
如果其他人在IB中遇到视图渲染问题,我发现将Bundle.main更改为Bundle(for: ...)可以解决问题。显然,在设计时Bundle.main不可用。 - crizzis
5
这个不适用于 @IBDesignable 的原因是你没有实现 init(frame:)。一旦你添加了这个(并把所有的初始化代码放到 commonInit() 函数里),它就可以正常工作并在 IB 中显示了。更多信息请参考这里:https://dev59.com/Sl0Z5IYBdhLWcg3wnBVS - Dimitris
显示剩余9条评论

144

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

要在IB中实例化计时器,请简单地将一个UIView拖到您的视图控制器的视图上,并将其类设置为您的计时器的类名。

enter image description here

请记得在您的视图控制器中 #import 您的计时器类。

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

我对Storyboard并不熟悉,但我知道您可以使用 .xib 文件在IB中构建界面,这与使用Storyboard版本几乎相同;您甚至可以将现有界面的整个视图复制并粘贴到 .xib 文件中。

为了测试此功能,我创建了一个名为 "MyCustomTimerView.xib" 的新空白 .xib。然后我添加了一个视图,以及标签和两个按钮。如下所示:

enter image description here

我创建了一个名为 "MyCustomTimer" 的 Objective-C 类,它是 UIView 的子类。在我的 .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可以大幅减少设置所需的时间。并且,由于你正在使用Storyboard加载计时器,我们知道只有-(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文件并将"File's Owner"属性设置为当前对象。我们从数组中提取第一个对象,这就是Nib的根视图。然后我们将该视图添加为子视图,然后Ta就出现在屏幕上了。
显然,当按钮被按下时,这只是改变标签的背景颜色,但这个例子应该能帮助你理解这方面的知识。
基于评论的说明:
值得注意的是,如果您遇到无限递归问题,那么您可能忽略了此解决方案微妙的技巧。它不是在做您想象中的事情。在Storyboard中放置的视图不可见,而是将另一个视图加载为子视图。它加载的视图是在Nib文件中定义的视图,而Nib文件中的“file's owner”就是该不可见视图。有趣的是,这个不可见的视图仍然是Objective-C类,可以作为某种视图控制器来使用所引入的视图。例如,在MyCustomTimer类中的IBAction方法通常更适用于视图控制器而不是视图。
作为一个附注,有些人可能会认为这违反了MVC模式,我也有同感。从我的角度来看,这更接近于自定义UITableViewCell,有时也必须成为控制器的一部分。
还值得注意的是,此答案提供了一个非常特定的解决方案;创建一个Nib文件,可以在Storyboard上多次实例化该文件。例如,在iPad屏幕上可以轻松想象出六个这样的计时器。如果您只需要为要在整个应用程序中多次使用的视图控制器指定一个视图,则问题的jyavenard提供的解决方案几乎肯定是更好的解决方案。

这实际上看起来像是一个单独的问题。但我认为可以很快地回答,所以我来了。每次创建新通知时,它都是一个新通知,但如果您需要它们具有相同的名称并想要添加一些区别它们的内容,则可以在通知的userInfo属性上使用它:@property(nonatomic,copy) NSDictionary *userInfo; - NJones
28
我从initWithCoder中得到一个无限递归。有任何想法吗? - xjq233p_1
6
抱歉搬起老帖子,确保“文件所有者(File's Owner)”设置为你的类名解决了我的无限递归问题。 - themanatuf
这是我找到的关于将自定义视图/xib添加到另一个视图的最清晰的答案。然而,我在我的视图大小方面遇到了问题。我通过将名为“view”的插座连接到我的xib中的基本UIView上,并将其连接到我的自定义UIView子类,然后在initWithCoder方法中的addSubView方法之后使用_view.frame = self.frame;来设置框架来“解决”它。 - MathewS
20
无限递归的另一个原因是将xib中的根视图设置为自定义类。您不应该这样做,将其保留为默认的“UIView”。 - X.Y.
11
这种方法有一点需要注意...难道没有人觉得两个视图现在在同一个类中很困扰吗?第一个是继承自UIView的原始文件.h/.m,通过执行 [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]];添加了第二个UIView... 这对我来说看起来有点奇怪,没人有同感吗?如果是,这是苹果设计的东西吗?还是别人发现的解决方案? - S.H.

39

解答关于视图控制器,而非视图的问题:

有一种更简单的方法从故事板中加载xib。 假设你的控制器是MyClassController类型,它继承自UIViewController

在故事板中添加一个UIViewController,将类类型更改为MyClassController。删除自动添加到故事板中的视图。

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

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


1
我经常充气自定义视图,但从未知道这个小技巧如何将其加载到Storyboard中。非常感谢! - MrTristan
39
这个问题涉及到视图(views),而不是视图控制器(view controllers)。 - Ricardo
为了澄清,添加了标题:“处理视图控制器而非视图的答案:” - nacho4d
1
似乎在iOS 9.1中无法工作。如果我删除自动创建的(默认)视图,在从VC的viewDidLoad返回后它会崩溃。 我认为XIB中的视图没有连接到默认视图。 - Jeff
1
我得到的异常是'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''。请注意这个奇怪的nibName。在nib中,我使用了正确的类名。 - Jeff

16

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

Objective-C

  1. CustomViewWithXib.hCustomViewWithXib.m 导入到你的项目中
  2. 使用相同的名称创建自定义视图文件(.h / .m / .xib)
  3. 从 CustomViewWithXib 继承你的自定义类

Swift

  1. CustomViewWithXib.swift 导入到你的项目中
  2. 使用相同的名称创建自定义视图文件(.swift and .xib)
  3. 从 CustomViewWithXib 继承你的自定义类

可选:

  1. 如果需要连接某些元素,请在 xib 文件中设置所有者为你的自定义类名(更多细节请参见 @Suragch 答案中的 Make the Swift file the owner 部分)

就是这样,现在你可以将你的自定义视图添加到 storyboard 中,并且它将被显示 :)

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 :


自定义带有Xib文件的视图。
#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)
    }
}

你可以在这里找到一些例子。

希望能有所帮助。


那么我们如何为视图UI控件添加IBOutlet呢? - Amr Angry
1
嘿@AmrAngry,要连接视图,您应该执行以下4个步骤: 进入您的xib文件,在“File's Owner”中设置您的类,并通过点击“ctrl”触摸将您的视图拖放到界面上。我使用此示例更新了代码源,如果您有任何问题,请不要犹豫,希望能帮助到您。 - HamzaGhazouani
非常感谢您的回复,我已经尝试过这个方法了,但我认为我的问题不在这个部分。 我的问题是我添加了一个UIDatePicker控件,并尝试从ViewController为此控件添加目标操作以设置和uiControleer事件值更改,但该方法从未被调用/触发。 - Amr Angry
1
@AmrAngry 我通过添加选择器视图更新了示例,也许这会对你有所帮助 :) https://github.com/HamzaGhazouani/Stackoverflow/tree/master/CustomViewsProject - HamzaGhazouani
1
谢谢,我已经寻找这个很长时间了。 - MH175

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接