最近完成了我司iOS项目的重构,把整体的代码架构都梳理了一遍,主要按照MVP的架构模式,并综合考虑了重构的难度和效果。在这个过程中也积累了一些代码重构方面的经验,在这里总结一下。

客户端开发项目中,不可避免地需要解析网络数据—将服务端下发的JSON数据解析成客户端可阅读友好的Model。Objective-C下使用最多的是JSONModel,它能在OC
Runtime基础下很好地完成解析工作。那么在纯Swift代码中,这个功能是如何实现的?下面开始我们的探索~

前言

大家都知道iOS的UIColor提供的自定义初始化是通过RGB
4个10进行制的参数,那么为什么用16进制呢?因为web和安卓端以常识性用16进制读取,所以为了方便设计师出图标注,很多时候都是16进制的效果图。而今天就是要讨论一下,这个16进制的转换的方法。

网页颜色 – 维基百科:

在HTML和CSS中使用3字节共6个十六进制数字表示一种颜色,每字节从00到FF,相当十进位数字从0到255,按顺序前两位是红色的值,中间两位是绿的值,最后两位是蓝色的值。

由于网页是基于计算机浏览器开发的媒体,所以颜色以光学颜色RGB为主。
网页颜色是以16进制代码表示,一般以“#”号开头,后面分别为R、G、B的16位进制数。
FF为最大数,代表十进制255。比如白色是R、G、B三个颜色最大,在网页代码便是:#FFFFFF。黑色是三个颜色为0,在网页代码便是:#000000。当颜色代码为#XXYYZZ时,可以用#XYZ表示,如#135与#113355表示同样的颜色。在CSS中,也可以使用rgb(127,127,127)代替#7F7F7F。

有意思的是,所以可以表示的颜色数总共有: 256^3 = 16,777,216
种,这个颜色就是我们常说的真彩色

项目简介

首先简单介绍一下项目情况。我们原有项目的架构是比较标准的MVC模式,也是苹果官方推荐的架构模式。Model层用来表示实体类,View层负责界面展示和传递UI事件,Controller层负责大部分的业务逻辑。除此之外,对一部分公共的可复用的逻辑,我们抽象出Service层,提供给Controller使用,另外网络层也独立出来。下图比较清楚地展示了整体架构

图片 1整体架构

  1. 手动解析
  2. 原生:Swift4.0 JSONDecoder
  3. JSONDecoder 问题 及 解决方案

几种16进制颜色值转换UIColor的方式

16进制转换为10进制后的整形转换为UIColor:

// rgb颜色转换(rgbValue为16进制转换成10进制的整形)#define MACRO_COLOR_HexCOLOR ([UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16))/255.0f green:((rgbValue & 0xFF00) >> 8))/255.0f blue:(rgbValue & 0xFF))/255.0f alpha:1.0])
  • 方法一:

#pragma mark -16进制颜色值转化UIColor+ (UIColor *)colorFromHexString:(NSString *)hexString{ unsigned rgbValue = 0; NSScanner *scanner = [NSScanner scannerWithString:hexString]; [scanner setScanLocation:1]; // bypass '#' character [scanner scanHexInt:&rgbValue]; // Optionally prefixed with "0x" or "0X" return MACRO_COLOR_HexCOLOR;}

这种方法,只能输入以#开头的十六进制颜色,比如#3c93fd,如果输入0X3c93fd,最后都会得到黑色。(因为rgbValue为0,所以全部颜色为0,得到黑色。)

  • 方法二:

+ (UIColor *)colorWithHexString:(NSString *) hexString { NSScanner *scanner = [NSScanner scannerWithString: hexString]; unsigned hexNum; if (![scanner scanHexInt:&hexNum]) return nil; return MACRO_COLOR_HexCOLOR;}

这种方法,只能输入以0X0x开头的十六进制颜色,比如0x3c93fd,如果输入#3c93fd,最后都会得到nil。(scanner转换10进制失败,hexNum为0,但是直接return
nil,)

  • 方法三:

// 颜色转换三:iOS中十六进制的颜色转换为UIColor+ (UIColor *) colorWithHexString: (NSString *)color{ NSString *cString = [[color stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString]; // String should be 6 or 8 characters if ([cString length] < 6) { return [UIColor clearColor]; } // 判断前缀并剪切掉 if ([cString hasPrefix:@"0X"]) cString = [cString substringFromIndex:2]; if ([cString hasPrefix:@"#"]) cString = [cString substringFromIndex:1]; if ([cString length] != 6) return [UIColor clearColor]; // 从六位数值中找到RGB对应的位数并转换 NSRange range; range.location = 0; range.length = 2; //R、G、B NSString *rString = [cString substringWithRange:range]; range.location = 2; NSString *gString = [cString substringWithRange:range]; range.location = 4; NSString *bString = [cString substringWithRange:range]; // Scan values unsigned int r, g, b; [[NSScanner scannerWithString:rString] scanHexInt:&r]; [[NSScanner scannerWithString:gString] scanHexInt:&g]; [[NSScanner scannerWithString:bString] scanHexInt:&b]; return [UIColor colorWithRed: r / 255.0f) green: g / 255.0f) blue: b / 255.0f) alpha:1.0f];}作者:艳晓链接:https://www.jianshu.com/p/96835798e4fc

这种方法,做了0x#判断,但是为什么那么长!!!有没有办法优化?

MVC模式的问题

MVC架构作为苹果官方推荐的架构模式,把数据Model和展现View通过Controller层隔离开,在项目规模较小的时候是一个不错的选择。随着项目复杂性的提高,我们也渐渐感觉到MVC模式的弊端,主要体现在下面几个方面

  • Controller层职责过多,Model和View层太简单Controller处理业务逻辑,处理UI更新,处理UI事件,同步Model层,我们几乎所有的代码都写在了Controller层。设计模式里有单一模式原则,你看这里的Controller层已经至少有四种职责了。
  • 业务逻辑和UI混杂在一起,难以编写单元测试这一点一方面是因为Cocoa框架里的Controller层,就是我们最熟悉的UIViewController和View是天然耦合的,很多view的生命周期方法如viewWillAppear都存在于VC,另一方面我们很多时候也习惯于把UI操作甚至初始化操作放在VC里,导致UI和业务逻辑混杂在一起。当你想对业务逻辑编写单元测试的时候,看着业务逻辑代码里混杂的UI操作,就知道什么叫举步维艰——数据可以Mock,UI是不可能被Mock的。
  • 业务逻辑代码大量存在于Controller层,维护困难当一个界面功能比较复杂的时候,我们所有的逻辑代码都会堆积在Controller中,比如我们原有的WebViewController的代码就多达5000行,在这种情况下维护代码简直是如履薄冰。

假设一个User类要解析,Json如下:

优化的方法?

因为用了NSScanner进行转换,所以为什么不用NSScanner直接判断,然后转换呢?另外苹果文档注释:

- scanHexInt:(nullable unsigned *)result; // Optionally prefixed with "0x" or "0X"

所以,其实用NSScanner进行转换时,不用判断 “0x” or “0X”,所以直接判断
#后过滤掉就可以啦!!!

直接看代码吧,发现看代码比文字更有力!:

+ (UIColor *) colorWithHexString:(NSString *)hexString { NSScanner *scanner = [NSScanner scannerWithString:hexString]; //从当前的扫描位置开始扫描,判断扫描字符串是否从当前位置能扫描到和传入字符串相同的一串字符,如果能扫描到就返回YES,指针指向的地址存储的就是这段字符串的内容。 [scanner scanString:@"#" intoString:NULL]; unsigned rgbValue = 0; [scanner scanHexInt:&rgbValue];// Optionally prefixed with "0x" or "0X" return MACRO_COLOR_HexCOLOR;}

上面的scanString: intoString:方法是一个技巧,判断是否包含#,如果包括时,scanner的scanLocation会指向下一个字符。所以用scanHexInt:将十六进制无符号整形时,已经不包含#,所以能成功转换。

MVP模式的重构

对于Controller层过于臃肿的问题,MVP模式则能较好地解决这个问题——既然UIViewControllerUIView是耦合的,索性把这两者都归为View层,业务逻辑则独立存在于Presenter层,Model层保持不变。下图比较清除得展示了MVP模式的结构

图片 2MVP模式简介我们来看一下MVP模式能否解决MVC模式存在的问题

  • Controller层职责过多,Model和View层太简单在MVP模式下,Controller层和View层已经合并为View层,专门负责处理UI更新和事件传递,Model层还是作为实体类。原本写在ViewController层的业务逻辑已经迁移到Presenter中。MVP模式较好地解决了Controller层职责过多的问题。

  • 业务逻辑和UI混杂在一起,难以编写单元测试Presenter层主要处理业务逻辑,ViewController层实现Presenter提供的接口,Presenter通过接口去更新View,这样就实现了业务逻辑和UI解耦。如果我们要编写单元测试的话,只需要Mock一个对象实现Presenter提供的接口就好了。MVP模式较好地解决了UI和逻辑的解耦。

  • 业务逻辑代码大量存在于Controller层,维护困难通过把业务逻辑迁移到Presenter层,Controller层的困境似乎得到了解决,但是如果某个需求逻辑较为复杂,单纯的把业务逻辑迁移解决不了根本的问题,Presenter层也会存在大量业务逻辑代码,维护困难。这个问题,我们下面会讨论如何解决。

{ "userId": 1, "name": "Jack", "height": 1.7,}

参考引用

  • uicolor-utilities/UIColor-Expanded.m at master ·
    kballard/uicolor-utilities
  • iOS中十六进制的颜色转换UIColor – 简书
  • NSScanner:一个陌生的条件判断利器 – 简书
  • NSScanner类的基本用法 – CSDN博客
  • NSScanner使用详解 – 简书
  • 网页颜色 – 维基百科

注:本文首发于 iHTCboy’s blog,如若转载,请注明来源。

MVC模式改进——Router模式

这里主要是考虑界面间跳转的代码如何重构,这一点我在之前的文章里已经有提到了,这里给个链接iOS重构之面向协议编程实践,另外附图一张

图片 3Router模式

前面我们提到,MVP模式虽然能解决许多MVC模式下存在的问题,但对于比较复杂的需求,还是会存在逻辑过于复杂,Presenter层也出现难以维护的问题。下面我们就通过一个实际的例子,来看看面对复杂的业务逻辑,我们应该如何去设计和实现。

很多复杂的需求,在最初都是从一个简单的场景,一步步往上增加功能。在这个过程中,如果不持续的进行优化和重构,到最后就成了所谓的”只有上帝能看懂的代码”。说了这么多,进入正题,来看这个需求。

对应的创建一个User结构体:

V1.0 单文件上传

实现一个简单的单文件上传,文件的索引存储在数据库中,文件存储在App的沙箱里面。这个应该对于有经验的客户端开发者来说是小菜一碟,比较简单也容易实现。我们可以把这个需求大致拆分成以下几个子需求

  1. 初始化上传View
  2. 更新上传View
  3. 点击上传按钮事件
  4. 数据库中获取上传模型
  5. 发起HTTP请求上传文件
  6. 检查网络状态

以上几项如果使用传统的MVC模式,实现起来如下图所示

图片 4MVC我们可以看到上述需求基本都直接在UploadViewController中实现,目前需求还是比较简单的情形下面,还是勉强能够接受,也不需要更多的思考。如果使用MVP的模式进行优化,如下图所示图片 5MVP.png

现在UploadPresenter负责处理上传逻辑了,而UploadViewController专注于UI更新和事件传递,整体的结构更加清晰,以后维护代码也会比较方便。

struct User { var userId: Int? var name: String? var height: CGFloat?}

发表评论

电子邮件地址不会被公开。 必填项已用*标注