iPhone开发中实现自定义控件
想在iPhone开发中实现自定义控件,查了很多资料,没有得到满意的结果。于是自己尝试了一下,最终获得成功。
我需要一个能够在运行时动态加载的自定义控件,这样的要求在wp7中非常容易实现,无论是直接继承ContentControl或是创建用户控件(UserControl)都非常轻松简单,在iPhone开发中如何实现,对iPhone开发还是一个新手的我却是一个难题。
查阅了很多资料以及国内国外的论坛,发现在iPhone中实现自定义控件的思路和wp7很像:
1、完全自绘,类似于在wp7中直接继承ContentControl,通过代码绘制控件界面。
2、组合现有控件,类似于wp7中的用户控件,在独立的nib文件中设计控件界面,然后在运行时加载。
第1种方式在我这个需求中不考虑,因为界面有点复杂,而且需要引用iPhone的现有控件,完全靠代码创建,不仅编码烦琐困难,而且日后的维护和修改也是相当大的一个问题。
第2种方式在完成自定义TableViewCell时实现过,问题不大,于是着手用第2种方式实现我的需求。起初以为没什么问题,但在实现过程中才发现没有想像得那么简单。
首先iOS5.1 Library并没有提供一个控件基类,供开发者在自定义控件时通过继承、重写等方式完成自己的控件。想完成第2种方式的思路,只能通过创建自定义视图(UIView)来实现。好吧,我就通过自定义视图来实现(因此后面我说的自定义视图和自定义控件是同一个意思)。
首先在项目中添加一个新文件, 模板选择:iOS->User Interface->Empty。在这里我犯了第一个错误,我选择了View模板,结果发现界面大小不能调整,View模板应该是针对全屏界面而不是用户自定义大小界面的模板。
给这个文件命名为CrosswordCellView.xib(我的控件名称),设计界面,这一步没有什么问题。需要注意的是,我要为这个控件添加一个TouchUpInside事件,但是我刚才说了,apple并没有像微软那样提供一个控件基类,所以我也不知道如何为它添加事件……我采取的办法是在界面上添加了一个按纽,风格设置为Custom,大小覆盖整个控件并随控件大小而变化,置于界面中所有其它控件的上层,这样,我可以通过按纽的TouchUpInside事件来模拟控件的事件。当按纽的风格为Custom时,它相当于“透明”的,因此即可以捕获事件,又不会影响自定义控件的界面。
接下来我们开始实现自定义控件的代码。添加一个新的文件,模板选择:iOS->Cocoa Touch->Objective-C Class并从UIView继承。文件名和nib文件名相同,Xcode生成两个文件:CrosswordCellView.h和 CrosswordCellView.m。然后通过工具窗口的标识检查器把类和nib文件关联起来。
接下来我开始茫然了,我们知道通过标识检查器把类和nib文件关联后,可以通过类当中的代码控制界面上的控件,比如设置控件属性、响应事件等等,可是我如何在初始化控件的实例并把它动态加载到视图控制器中时,显示我的自定义控件界面呢?我知道在动态创建视图控制器时,StoryBoard有一个实例方法instantiateViewControllerWithIdentifier可以从StoryBoard文件中创建视图控制器,但是我查遍了UIView的文档,却没有发现类似的从nib文件创建视图的方法。
在关于UIView的文档中,我看到一段对initWithCoder方法的说明:如果你要从nib文件中创建视图,你需要重写这个方法。问题是怎么重写?怎么对nib文件解码?因为initWithCoder的方法的参数是一个 NSCoder *类型,我自然把它看成需要对nib文件解码,并把解码后生成的NSCoder *类型的变量传递给initWidthCoder方法,可是找遍了文档和论坛,没有发现关于这个问题的解答。
后来我决定不在这个问题上纠结,于是在网上搜索了一些关于iPhone自定义控件的文章,才发现我的想法是错误的,initWithCoder方法是在控件初始化时由基类自动调用的,自动调用的前提是使用NSBundle的实例方法loadNibNamed:owner:options:加载nib文件。使用这个方法时我们需要指定自定义视图的File's Owner,这和我之前在实现自定义TableViewCell的方式一样。即把自定义视图的File's Owner指定为要加载这个视图的视图控制器,在视图控制器中声明一个自定义视图的IBOutlet,并把它和自定义视图关联到一起。具体做法是:选择自定义视图文件CrosswordCellView.nib,在左边选择如图
所示的File's Owner,在右边的标志检查器中选择视图控制器,通过下拉框选择你的视图控制器。网上所有的例子都是把nib文件的File's Owner指定为视图控制器,这样一来,自定义控件是无法在其它视图控制器中使用的,除非你在需要这个自定义控件的视图中也添加一个相应的IBOutlet。并且,这样一来,无法实现动态添加。在我的需求中,一个视图控制器会包含几十个我的自定义控件,显然不能按网上的方法去实现。
在这个例子中,我在CrosswordCellView的类文件中定义了一个关联到自定义视图的IBOutlet,同时把CrosswordCellView.xib的File's Owner也指向CrosswordCellView类,因为我要让这个类显示我设计好的界面。
接下来修改类文件,在CrosswordCellView.h中:
#import <UIKit/UIKit.h>
@class CrosswordCellView;
//定义一个协议,以便在外部响应控件的TouchUpInside事件
@protocol CrosswordCellViewDelegate
- (void)viewDidClick:(CrosswordCellView *)sender;
@end
@interface CrosswordCellView : UIView
//其它属性略
//指向nib文件的IBOutlet
@property (nonatomic, weak)IBOutlet CrosswordCellView *subView;
//委托
@property (assign)id<CrosswordCellViewDelegate> delegate;
//自定义控件中按纽的TouchUpInside事件响应方法
- (IBAction)buttonClick:(id)sender;
@end
在CrosswordCellView.m中:
#import "CrosswordCellView.h"
@implementation CrosswordCellView
@synthesize delegate = _delegate, subView = _subView;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//加载nib文件,加载后,nib文件的设计界面将会呈现在_subView中
[[NSBundle mainBundle]loadNibNamed:@"CrosswordCell" owner:self options:nil];
CGRect viewFrame = frame;
viewFrame.origin = CGPointMake(0, 0);
_subView.frame = viewFrame;
//把nib文件的视图添加到本视图中
[self addSubview:_subView];
}
return self;
}
//重写这个方法可以加上一些自绘的效果
// 这里我给控件加上一个绿色的边框
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect frame = self.bounds;
CGContextSetLineWidth(context, 1);
[[UIColor greenColor]set];
UIRectFrame(frame);
}
//当按纽触发TouchUpInside事件,调用委托的方法
- (IBAction)buttonClick:(id)sender
{
[self.delegate viewDidClick:self];
}
@end
在视图控制器中,首先要实现CrosswordCellViewDelegate协议,这部分代码就不贴了,直接看实现文件。
ViewController.m文件:
#import "ViewController.h"
#import "CrosswordCellView.h"
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
CrosswordCellView *_wordView = [[CrosswordCellView alloc]initWithFrame:CGRectMake(10, 10, 40, 40)];
//定义委托对象
_wordView.delegate = self;
[self.view addSubview:_wordView];
}
-(void)viewDidClick:(CrosswordCellView *)sender
{
//这是我测试事件的代码
sender.labelWord.text=@"A";
}
接下来运行代码,动态声明控件并显示到视图控制器中,成功!
但是(我很讨厌这个词),当我点击控件时,控件上的内容并没有改变!我在 sender.labelWord.text=@"A";这里设置了一个断点,运行程序,点击控件,Xcode没有理会我,也就是说,sender.labelWord.text=@"A";没有执行到。
是代码有问题吗?我仔细检查了N遍,可以肯定协议的声明和实现没有任何问题,那这是怎么一回事,我真的要抓狂了。
还能怎么办?在一次次的跟踪执行后,我发现
- (IBAction)buttonClick:(id)sender
{
[self.delegate viewDidClick:self];
}
这里的delegate根本就是一个nil,可是我明明在视图控制器写了这行代码:
//定义委托对象
_wordView.delegate = self;
这到底是怎么回事?
反复跟踪后我发现,视图控制器中的_wordView和CrosswordCellView的self(上面两段代码中红色标出的)指向的是不同地址,也就是说这两个不是同一个对象。
对self,我一直把它理解成C#中的this,但从这里的情况看,显然不是这样。self到底代表什么,恐怕我要把关于Objective-C语言的书籍反反复复阅读多遍,并编写大量代码才能搞清楚,我实在没这个兴趣了。
通过跟踪代码,我发现self的成员_subView和_wordView指向的是同一个地址,也就是说,在视图控制器中实例化的
CrosswordCellView对象实际上指向的是它的成员_subView,这肯定跟_subView也是个 CrosswordCellView对象有关,但不应该这样啊!
我现在的解决办法是修改了CrosswordCellView.m的initWithFrame:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[[NSBundle mainBundle]loadNibNamed:@"CrosswordCell" owner:self options:nil];
CGRect viewFrame = frame;
viewFrame.origin = CGPointMake(0, 0);
_subView.frame = viewFrame;
[self addSubview:_subView];
self = _subView;
self.frame = frame;
}
return self;
}
把自定义控件的实例指向了它的成员_subView,从原理上说应该没有问题,实际执行也没有任何问题,现在我的CrosswordView可以正确地响应TouchUpInside事件了。
问题是,有没有人能告诉我,为什么会这样?
我需要一个能够在运行时动态加载的自定义控件,这样的要求在wp7中非常容易实现,无论是直接继承ContentControl或是创建用户控件(UserControl)都非常轻松简单,在iPhone开发中如何实现,对iPhone开发还是一个新手的我却是一个难题。
查阅了很多资料以及国内国外的论坛,发现在iPhone中实现自定义控件的思路和wp7很像:
1、完全自绘,类似于在wp7中直接继承ContentControl,通过代码绘制控件界面。
2、组合现有控件,类似于wp7中的用户控件,在独立的nib文件中设计控件界面,然后在运行时加载。
第1种方式在我这个需求中不考虑,因为界面有点复杂,而且需要引用iPhone的现有控件,完全靠代码创建,不仅编码烦琐困难,而且日后的维护和修改也是相当大的一个问题。
第2种方式在完成自定义TableViewCell时实现过,问题不大,于是着手用第2种方式实现我的需求。起初以为没什么问题,但在实现过程中才发现没有想像得那么简单。
首先iOS5.1 Library并没有提供一个控件基类,供开发者在自定义控件时通过继承、重写等方式完成自己的控件。想完成第2种方式的思路,只能通过创建自定义视图(UIView)来实现。好吧,我就通过自定义视图来实现(因此后面我说的自定义视图和自定义控件是同一个意思)。
首先在项目中添加一个新文件, 模板选择:iOS->User Interface->Empty。在这里我犯了第一个错误,我选择了View模板,结果发现界面大小不能调整,View模板应该是针对全屏界面而不是用户自定义大小界面的模板。
给这个文件命名为CrosswordCellView.xib(我的控件名称),设计界面,这一步没有什么问题。需要注意的是,我要为这个控件添加一个TouchUpInside事件,但是我刚才说了,apple并没有像微软那样提供一个控件基类,所以我也不知道如何为它添加事件……我采取的办法是在界面上添加了一个按纽,风格设置为Custom,大小覆盖整个控件并随控件大小而变化,置于界面中所有其它控件的上层,这样,我可以通过按纽的TouchUpInside事件来模拟控件的事件。当按纽的风格为Custom时,它相当于“透明”的,因此即可以捕获事件,又不会影响自定义控件的界面。
接下来我们开始实现自定义控件的代码。添加一个新的文件,模板选择:iOS->Cocoa Touch->Objective-C Class并从UIView继承。文件名和nib文件名相同,Xcode生成两个文件:CrosswordCellView.h和 CrosswordCellView.m。然后通过工具窗口的标识检查器把类和nib文件关联起来。
接下来我开始茫然了,我们知道通过标识检查器把类和nib文件关联后,可以通过类当中的代码控制界面上的控件,比如设置控件属性、响应事件等等,可是我如何在初始化控件的实例并把它动态加载到视图控制器中时,显示我的自定义控件界面呢?我知道在动态创建视图控制器时,StoryBoard有一个实例方法instantiateViewControllerWithIdentifier可以从StoryBoard文件中创建视图控制器,但是我查遍了UIView的文档,却没有发现类似的从nib文件创建视图的方法。
在关于UIView的文档中,我看到一段对initWithCoder方法的说明:如果你要从nib文件中创建视图,你需要重写这个方法。问题是怎么重写?怎么对nib文件解码?因为initWithCoder的方法的参数是一个 NSCoder *类型,我自然把它看成需要对nib文件解码,并把解码后生成的NSCoder *类型的变量传递给initWidthCoder方法,可是找遍了文档和论坛,没有发现关于这个问题的解答。
后来我决定不在这个问题上纠结,于是在网上搜索了一些关于iPhone自定义控件的文章,才发现我的想法是错误的,initWithCoder方法是在控件初始化时由基类自动调用的,自动调用的前提是使用NSBundle的实例方法loadNibNamed:owner:options:加载nib文件。使用这个方法时我们需要指定自定义视图的File's Owner,这和我之前在实现自定义TableViewCell的方式一样。即把自定义视图的File's Owner指定为要加载这个视图的视图控制器,在视图控制器中声明一个自定义视图的IBOutlet,并把它和自定义视图关联到一起。具体做法是:选择自定义视图文件CrosswordCellView.nib,在左边选择如图

在这个例子中,我在CrosswordCellView的类文件中定义了一个关联到自定义视图的IBOutlet,同时把CrosswordCellView.xib的File's Owner也指向CrosswordCellView类,因为我要让这个类显示我设计好的界面。
接下来修改类文件,在CrosswordCellView.h中:
#import <UIKit/UIKit.h>
@class CrosswordCellView;
//定义一个协议,以便在外部响应控件的TouchUpInside事件
@protocol CrosswordCellViewDelegate
- (void)viewDidClick:(CrosswordCellView *)sender;
@end
@interface CrosswordCellView : UIView
//其它属性略
//指向nib文件的IBOutlet
@property (nonatomic, weak)IBOutlet CrosswordCellView *subView;
//委托
@property (assign)id<CrosswordCellViewDelegate> delegate;
//自定义控件中按纽的TouchUpInside事件响应方法
- (IBAction)buttonClick:(id)sender;
@end
在CrosswordCellView.m中:
#import "CrosswordCellView.h"
@implementation CrosswordCellView
@synthesize delegate = _delegate, subView = _subView;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//加载nib文件,加载后,nib文件的设计界面将会呈现在_subView中
[[NSBundle mainBundle]loadNibNamed:@"CrosswordCell" owner:self options:nil];
CGRect viewFrame = frame;
viewFrame.origin = CGPointMake(0, 0);
_subView.frame = viewFrame;
//把nib文件的视图添加到本视图中
[self addSubview:_subView];
}
return self;
}
//重写这个方法可以加上一些自绘的效果
// 这里我给控件加上一个绿色的边框
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect frame = self.bounds;
CGContextSetLineWidth(context, 1);
[[UIColor greenColor]set];
UIRectFrame(frame);
}
//当按纽触发TouchUpInside事件,调用委托的方法
- (IBAction)buttonClick:(id)sender
{
[self.delegate viewDidClick:self];
}
@end
在视图控制器中,首先要实现CrosswordCellViewDelegate协议,这部分代码就不贴了,直接看实现文件。
ViewController.m文件:
#import "ViewController.h"
#import "CrosswordCellView.h"
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
CrosswordCellView *_wordView = [[CrosswordCellView alloc]initWithFrame:CGRectMake(10, 10, 40, 40)];
//定义委托对象
_wordView.delegate = self;
[self.view addSubview:_wordView];
}
-(void)viewDidClick:(CrosswordCellView *)sender
{
//这是我测试事件的代码
sender.labelWord.text=@"A";
}
接下来运行代码,动态声明控件并显示到视图控制器中,成功!
但是(我很讨厌这个词),当我点击控件时,控件上的内容并没有改变!我在 sender.labelWord.text=@"A";这里设置了一个断点,运行程序,点击控件,Xcode没有理会我,也就是说,sender.labelWord.text=@"A";没有执行到。
是代码有问题吗?我仔细检查了N遍,可以肯定协议的声明和实现没有任何问题,那这是怎么一回事,我真的要抓狂了。
还能怎么办?在一次次的跟踪执行后,我发现
- (IBAction)buttonClick:(id)sender
{
[self.delegate viewDidClick:self];
}
这里的delegate根本就是一个nil,可是我明明在视图控制器写了这行代码:
//定义委托对象
_wordView.delegate = self;
这到底是怎么回事?
反复跟踪后我发现,视图控制器中的_wordView和CrosswordCellView的self(上面两段代码中红色标出的)指向的是不同地址,也就是说这两个不是同一个对象。
对self,我一直把它理解成C#中的this,但从这里的情况看,显然不是这样。self到底代表什么,恐怕我要把关于Objective-C语言的书籍反反复复阅读多遍,并编写大量代码才能搞清楚,我实在没这个兴趣了。
通过跟踪代码,我发现self的成员_subView和_wordView指向的是同一个地址,也就是说,在视图控制器中实例化的
CrosswordCellView对象实际上指向的是它的成员_subView,这肯定跟_subView也是个 CrosswordCellView对象有关,但不应该这样啊!
我现在的解决办法是修改了CrosswordCellView.m的initWithFrame:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[[NSBundle mainBundle]loadNibNamed:@"CrosswordCell" owner:self options:nil];
CGRect viewFrame = frame;
viewFrame.origin = CGPointMake(0, 0);
_subView.frame = viewFrame;
[self addSubview:_subView];
self = _subView;
self.frame = frame;
}
return self;
}
把自定义控件的实例指向了它的成员_subView,从原理上说应该没有问题,实际执行也没有任何问题,现在我的CrosswordView可以正确地响应TouchUpInside事件了。
问题是,有没有人能告诉我,为什么会这样?

浙公网安备 33010602011771号