iOS

如何写一个控件

写控件,是做客户端开发的一项基本功。为什么这么说呢?

#前言

写控件,是做客户端开发的一项基本功。为什么这么说呢?首先,写一个控件并不需要大量的代码(绝大部分1000行代码就可以搞定);其次,写控件并不难,只要给出视觉效果,不管新手还是老手,都能够最终搞定(无非是时间长短而已);但我觉得最重要的是,写控件很能体现出抽象和面向对象的思想。高质量的控件,调用者只需要很少的代码,就能够嵌入到不同的使用场景;而实现质量堪忧的控件,调用端写一大堆代码不说,需求上稍有变化,可能就要推倒重来了。最近正好在实现一个控件,花了整整一个工作日,虽然这个控件还有很多问题,但回想起过程中的一些思路和方法,觉得有必要总结下。

#需求

这是一个筛选控件,常见得不能再常见了,几乎是个APP都有这样的控件。直接上图:

control_1.png

control_2.png

control_3.png

control_4.png

简单解释一下:
1、控件有两种类型,一种是有图片的(control_1.png),一种是没图片的(control_3.png)。
2、控件被选中的时候下方动画展开菜单,菜单高度根据菜单项的数量决定,但高度有上限。
3、菜单项选择了之后会标题按钮会更新成选择的文字,同时菜单项收回。
4、菜单项弹出式遮罩整个屏幕,点击遮罩部分,同时菜单项收回。
5、菜单分隔项数量,标题和图片,以及菜单项按钮的文字都是可变的。

#目标

有了需求,就要开始写了。但写之前,先要定个目标。因为写控件这种东西,没有产品经理来验收,所以不定目标的话,很容易自我妥协。

对于控件,我的目标一贯就是:让调用者写最少的代码。

这样有两个好处:

  • 减轻调用者的工作负担
  • 减少调用者写错代码的可能性

那么问题就来了:我想让对方用一行代码就调起控件,可是可变得东西太多,比如到底有没有图片,有几个筛选项,按钮和菜单的文字又是什么?所以对于这个控件,一行代码有点太天真了,有一部分数据必须是由调用者提供的。究竟是什么数据呢?先看我的第一个观点:

###观点一:可变性与不变性

所谓控件,是对UI设计的某种抽象。对于产品经理来说,并没有控件的概念,他只是把需求画在纸上,告诉你交互的逻辑,然后叫你实现。然后程序员按照需求画出了界面,似乎没有用到什么控件。

可不幸的是,第二天,产品经理又要改了,比如说把文字变大点,然后加粗,或者加个动画什么的。于是,你又重新把代码改了一遍。

可是,第三天、第四天……你怒了!不过为了保住饭碗,只能忍气吞声。

真的只能忍气吞声吗?不!那样太消极了。你难道就没有想过,每次产品经理改来改去,总有一些东西是不会变的,而那些变来变去的东西往往也就那么几样!

当你想到这一点的时候,你已经不再是菜鸟了,因为你已经了解到软件设计最灵魂的两个字:抽象。

抽象就是把一堆各不相同的东西看成一个。比如世界上每个人都不一样,但他们都是人。把不同的UI设计进行抽象,就是控件。从系统层面上来讲,UIView、UIButton、UILabel等等,都是控件。它们都有其本质的特点以区别于其他控件,但他们又具有丰富的扩展性,能够产生千变万化的效果。但万变又不离其中,Label还是Label,Button还是Button。

好像扯远了,收回来。我们现在要把上图的UI效果做成控件,就要知道哪些东西是可变的,哪些东西是不变的。

不变的:

  • 初始状态肯定是并排几个按钮,点一下下面的菜单就会展开,再点一下有收回
  • 选中的按钮会有特殊状态以区别于其他按钮。
  • 展开的菜单上面都显示字符串
  • 点击展开菜单中的某一项,就会出发某个事件,然后菜单收回。

可变的:

  • 按钮数量、图片、文字
  • 展开的菜单项数量,菜单项内容。
  • 选中项的位置(要高亮的位置)

如果真要严格的讲,可变的内容太多了。但你要知道,可变即意味着控件自身不可知,只能从外部获取,这样就加大了调用者的工作量。所以,在实际设计控件时,我们只需提取几个最有可能发生变化的因素,其它的就当它是固定的即可。比如在这个例子中,弹出菜单的每一项的具体高度,是不是统一的?我就把它当成全部都是一样的,而且都是40,因为这种变化确实不太可能发生。但是像按钮和菜单项的文字,那是绝对会变的(不然还怎么扩展)。

总结一下,就是把可变的因素中最有可能的几个因素罗列出来,传给控件,然后通过控件的内部各种固定逻辑将其展现出来。

###观点二:主动传参与被动传参的选择

先解释一下主动传参与被动传参的概念,因为这是我自己脑补的。
主动传参:就是通过参数、属性的形式把参数传给控件。

1
2
3
4
5
self.menuView = [[LCSegmentMenuView alloc] initWithFrame:rect
containerView:self.view
itemCount:3
style:LCSegmentMenuStyle_Text];
self.menuView.delegate = self;

上面是控件调用代码,Frame、containerView:self、itemCount、style、delegate,这些都属于主动传参。它的特点除了传递方式之外,时间上都是先于控件控件自身逻辑运行之前。

被动传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
- (NSString*)segmentMenuView:(LCSegmentMenuView*)segmentMenuView titleAtSection:(NSUInteger)section
{
return self.menuTitles[section];
}

- (UIImage*)segmentMenuView:(LCSegmentMenuView*)segmentMenuView imageAtSection:(NSUInteger)section
{
return self.menuImages[section];
}

- (UIImage*)segmentMenuView:(LCSegmentMenuView*)segmentMenuView selectedImageAtSection:(NSUInteger)section
{
return self.menuSelectedImages[section];
}

- (NSArray*)titlesInSegmentMenuView:(LCSegmentMenuView*)segmentMenuView atSection:(NSUInteger)section
{
if (section == 0) {
return @[NSLocalizedString(@"Total", nil),@"5km",@"10km",@"15km"];
}
else if(section == 1){

NSString* stringA = NSLocalizedString(@"Total", nil);
NSString* stringB = NSLocalizedString(@"Wash car", nil);
NSString* stringC = NSLocalizedString(@"repair and maintain", nil);
NSString* stringD = NSLocalizedString(@"Cosmetology", nil);
return @[stringA,stringB,stringC,stringD];
}
else if(section == 2){

NSString* stringA = NSLocalizedString(@"Distance from near to far", nil);
NSString* stringB = NSLocalizedString(@"Praise from high to low", nil);
NSString* stringC = NSLocalizedString(@"Sales from high to low", nil);
return @[stringA,stringB,stringC];
}
return nil;
}

- (NSUInteger)currentIndexInSegmentMenuView:(LCSegmentMenuView*)segmentMenuView atSection:(NSUInteger)section
{
return [self.menuIndexs[section] integerValue];
}

- (void)segmentMenuView:(LCSegmentMenuView*)segmentMenuView didSelectAtIndexPath:(NSIndexPath*)indexPath
{
self.menuIndexs[indexPath.section] = @(indexPath.row);
NSArray* array = [self titlesInSegmentMenuView:segmentMenuView atSection:indexPath.section];
self.menuTitles[indexPath.section] = array[indexPath.row];

[self.parentVC.shops removeAllObjects];
self.currentPage = 0;
[self getShopList:YES];
}

上面几个方法,一看就是代理方法,这些方法提供的数据,可不是控件创建之初就知道的,而是由于人为的交互,改变的控件的内部状态,进而产生UI上的变化。对于调用者而言,这些数据都是被动的有控件来索取,所以称为被动传参。

在系统控件中,像UILabel全是主动传参,因为它没有交互;像UITableView,就有很多被动传参。被动传参除了Delegate,还可以用Block,KVO实现,主动传参就直来直去,简单的多。

总结一下,不到万不得已,尽量使用主动传参。但主动传参局限性太大了,只能发生在事前,不能发生在事中。所以,交互稍微复杂一点的控件,就要使用被控传参。

观点三:控件结构化

上面提到传参,我们就穿了一堆参数。但是参数太多好乱,怎么办?原因就是没有结构化。

我们设计完控件,控件它就是一个对象,比如:LCSegmentMenuView。这个对象对应了一堆参数,什么title、index、image,这些零散的东西为什么不可以结构化成一个对象呢?这样我们的控件每次取外部数据,只要从这个结构化对象里面取就可以了(只要为这个对象增加几个对外接口)。
这样做的好处是显而易见的,本来外部调用者要管理如下的参数

1
2
3
4
@property (nonatomic,strong) NSMutableArray* menuTitles;
@property (nonatomic,strong) NSArray* menuImages;
@property (nonatomic,strong) NSArray* menuSelectedImages;
@property (nonatomic,strong) NSMutableArray* menuIndexs;

把它们结构化成一个对象之后,只要管理一个变量即可

1
@property (nonatomic,strong) LCMenu* menu;

这样就能避免很多因为代码零散而造成的复杂性。这样一个对象从抽象的角度来讲也是应该存在的,它就是控件的灵魂,没有它,控件就是一堆代码;有了它,控件遍丰富多彩。

可惜目前我还没有封装这个对象,因为我觉得四个变量管理起来也不是很复杂,反正要比新写一个对象简单。但如果变成了8个变量,我想我一定会去封装这个对象的。

#总结

说了这么多,具体控件实现没有讲。因为这个实在没什么好讲的,就是组装系统控件而已。等这个控件完善了,直接贴代码好了。