[贝聊科技]iOS 代码架构(一)如何创建一个易复用的组件

作者 子豪 贝聊iOS工程师

前言

贝聊的移动客户端分别有家长端和老师端,一家公司里同时维护多个业务上有关联性的app这种情况其实很常见,例如一些提供 O2O 服务的公司,经常会分用户端和商家端。这些客户端虽然各自负责着一个业务环里面的不同部分,看似不相关,但其实内在的设计、代码都有很多共同之处。

我们编写代码时一条最重要的军规是 DRY (Don't Repeat Yourself),意思就是同样或者相似的代码只写一次,通过代码复用的技巧做成公用的组件。项目工期紧张时,其他的一些编码守则都可以稍微变通一下,但唯独 DRY 是绝对要遵守的。这样做最大的好处是当发生需求变更、重构或者修复 bug 时,只要改动一处的代码就可以了。如果采用到处 copy 代码的方式,则需要在每一处引用到的地方做修改,很容易就会出现遗漏。并且时间一长,这些复制的代码很容易会渐行渐远,衍生出许多不同的分支,维护难度呈指数级上升。稍有经验的程序员应该都知道到处拷代码就是挖坑的开始,本文以一个较简单的 UI 组件为例,介绍贝聊 iOS 组在设计可复用组件时的一点小技巧。

遇到的问题

贝聊的家长版和老师版针对的受众不同,设计语言、配色等方面也有点不同,以最简单的自定义提示框为例:

家长版:

老师版:

主要的不同点:

  1. 按钮的圆角半径 (cornerRadius)
  2. 按钮的大小、位置 (frame)
  3. 文字的字号 (fontSize)
  4. 文字内容到提示框边界的距离 (contentInsets)
  5. 其实之前连按钮的颜色都不一样,不过最近UI改版了

初看起来不同点很多,但仔细看其实只是一些设计上的元素有不同。事实上家长版和老师版的提示框其实底层用的都是同一套代码,这个弹框组件BLAlertController是我们 iOS 组一个新入行的小伙写的,很好地遵守了 DRY 原则,灵活性和代码质量都非常高。本文就用这个组件为例来说说,怎样在多个 app 之间优雅地复用代码。

创建一个配置类

先来看看初始化方法,alertController的命名是仿照系统的UIAlertController,但是因为 UI 是高度可定制的,所以多加入了很多参数。

+ (instancetype)alertControllerWithTitle:(NSString *)title 
                                         message:(NSString *)message
                               buttonTextColor:(UIColor *)textColor
                        buttonBackgroundColor:(UIColor *)buttonBackgroundColor
                                  cornerRadius:(CGFloat)cornerRadius
                   ....  // 篇幅原因,点击回调和其他配置项都省略,全部列出来的话超过二十项

这里遇到的第一个问题就是参数列表过长,Objective-C 没有默认参数也没有方法重载,如果每次初始化都要填写这一大堆参数,这样的组件也未免太难用了。

其实 iOS SDK 的代码里面就有很多优秀的设计模式的应用范例,遇到问题的时候参考一下,会有很多收获。这里遇到的问题主要是代码架构的问题,发散一下,发现 Foundation 框架的 NSURLSession也是有很多可配置的属性的。苹果的工程师把这些可选参数专门构造成了一个NSURLSessionConfiguration来管理这些可配置属性。创建一个NSURLSession时,需要传入一个NSURLSessionConfiguration来指定一些参数,而NSURLSessionConfiguration的大部分属性都是有默认值的,例如timeoutIntervalForRequest。通过NSURLSessionConfiguration.defaultSessionConfiguration方法可以创建一个默认的 configuration,此时timeoutIntervalForRequest的默认值是60,这个值能适用于大部分情况。如果有特殊的需求也可以自行调整。

我们在99%的情况下其实都只是想用默认样式的弹框,这时创建一个可定制的,带默认值的配置类就是很好的解决方法。

依葫芦画瓢,我们也创建一个BLAlertConfiguration,定义大致如下:

@interface BLAlertConfiguration : NSObject <NSCopying> // 配置类实现了深拷贝

@property (nonatomic) UIColor *buttonTextColor;
@property (nonatomic) UIColor *buttonBackgroundColor;
@property (nonatomic) CGFloat cornerRadius;

// 默认的配置项
@property (class, nonatomic) BLAlertConfiguration *defaultConfiguration;

... //其他可配置项由于篇幅原因不一一列举了

@end

@interface BLAlertController : UIViewController

- (instancetype)initWithTitle:(NSString *)title
                      message:(NSString *)message
                configuration:(BLAlertConfiguration *)configuration;
      
 - (instancetype)initWithTitle:(NSString *)title
                       message:(NSString *)message;
                       
@end

BLAlertController 有两个初始化方法,initWithTitle:message: 是个 convenience initializer,内部调用了 initWithTitle:message:configuration:并把BLAlertConfiguration.defaultConfiguration传进去了。所以一般的使用就很简单了,直接调用initWithTitle:message:就好。

在不同的项目中设置不同的默认值

上面解决了参数列表过长的问题,但是还是没有说明在两个项目中怎么设置不同的默认 UI 风格。答案其实呼之欲出,聪明的读者应该已经想到了。

BLAlertConfiguration.defaultConfiguration 这个属性是 Objective-C 新加的 class property 语法,用来打通 Swift 的类属性。我们可以通过静态变量和 getter setter,把 defaultConfiguration 变成一个可读可写的类属性。

@implementation BLAlertConfiguration

static BLAlertConfiguration *defaultConfiguration;

+ (void)setDefaultAlertConfiguration:(BLAlertConfiguration *)configuration {
    if (defaultConfiguration) { //只允许设置一次,有值的时候返回
        return;
    }
    defaultConfiguration = [configuration copy]; // 通过拷贝对象,避免配置项后面被修改
}

+ (instancetype)defaultConfiguration {
    NSAssert(defaultConfiguration, @"未设置 defaultConfiguration,应先调用 +[BLAlertConfiguration setDefaultAlertConfiguration:] 来进行初始化");
    return defaultConfiguration;
}

@end

这样只要在程序启动的时候,例如在 AppDelegate 的application:didFinishLaunchingWithOptions:回调中设置一下 BLAlertConfiguration.defaultConfiguration 就可以了,在不同的项目中设置不同的默认值,就能达成不同的设计风格。

BLAlertConfiguration *configuration = [BLAlertConfiguration new];
configuration.buttonTextColor = [UIColor blackColor];
configuration.buttonBackgroundColor = [UIColor yellowColor];
configuration.cornerRadius = 4.0;

BLAlertConfiguration.defaultAlertConfiguration = configuration;

结语

本文作为系统的首篇和引子,内容相对简单,但是很好地体现了 DRY 的精神。如果你很少接触这类问题,这会是一个很好的开始。学会发现代码中的坏味道,思考改进的方法,保持项目整洁是提升架构设计能力的必经之路。这种代码复用的手法目前已经贯穿了我们整个公共代码库,并且有很多变体,后面会陆续地介绍贝聊项目中其他关于代码复用方面的心得,敬请期待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容