Object-C工程实践2018

背景介绍

互联网金融行业,一般是工具类的产品。一种是P2P,另外一种就是钱包,当然支付宝就是一个最大的钱包。
简单点,就用Wallet作为项目名字,bundle ID就定义为com.xxx.wallet

创建工程

通过XCode创建一个新工程,比较简单。这里要选的就是语言用Object-C还是Swift。这是一个很让人纠结的问题,怎么选都对,也都感觉不爽。

  • 虽然Swift是未来,不过现状还是Object-C占比较大,所以保守起见,还是选Object-C
  • 工程类型一般就默认的Single View App好了,反正以后都要改,默认的文件都会被删掉。
  • UI TestUnit Test前面的勾都去掉,说实话,在客户端开发中,这两个还真没什么大用处。
image.png
  • 版本控制,目前主流是git,没什么好纠结的,相比之下,svn相差太大。至于是用命令行,还是用XCode自带的,或者是第三方工具,根据个人自己的喜好来定。本人习惯了Sourece Tree,所以一般就用这个了,所以这里的勾要去掉。实际上命令行和XCode自带的git工具都很好用。
image.png
  • .gitignore文件。这是用来说明哪些文件不进行版本控制,比如build文件夹下面的内容就没有必要提交到版本库。这是一个隐藏文件,可以通过快捷键shift+cmmand+.来切换显示和隐藏。
image.png
  • .gitignore文件具体内容,可以上GitHub上去查Objective-C 版
    也可以上网站gitignore.io去生成,只要输入关键字Objective-C,点击Create按钮就可以生成相关的内容。

  • .gitignore虽然是隐藏文件,也要加入版本管理,不能忽略。

包管理工具

在语言选定Object-C的情况下,第三方库的管理工具基本上就是CocoaPods了。如果是Swift的话,还要纠结一下是否用Carthage

  • CocoaPods是一个用Ruby写的工具,需要事先安装。由于国内防火墙的原因,需要注意一些事情,详情可以参考下面这篇文章
    最新的CocoaPods的使用教程(一)

  • 切换到工程目录,执行命令pod init,生成文件Podfile,修改文件内容,以适合工程的需要。

# Uncomment the next line to define a global platform for your project
# 最低支持版本
platform :ios, '10.0'
# 去掉第三方库的warning
inhibit_all_warnings!

target 'Wallet' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # 目前framework是主流
  use_frameworks!

  # Pods for HaiLeBao
  # 网络库
  pod 'AFNetworking'
  # 代码加限制
  pod 'Masonry'
  # Hud,Toast等
  pod 'SVProgressHUD'
  # 表格上拉下拉
  pod 'MJRefresh'
  # 缓存
  pod 'YYCache'
  # 字典转模型
  pod 'YYModel'
  # 图片下载和处理,依赖YYImage
  pod 'YYWebImage'
  # 类别形式的工具
  pod 'YYCategories'
  # 友盟;基础库
  pod 'UMCCommon'
  pod 'UMCSecurityPlugins'
  # 友盟;统计 SDK
  pod 'UMCAnalytics'
  # 友盟;错误分析 SDK
  pod 'UMCErrorCatch'
  # U-Share SDK UI模块(分享面板,建议添加)
  pod 'UMCShare/UI'
  # 集成微信(完整版14.4M)
  pod 'UMCShare/Social/WeChat'
  # key chain,存用户名密码
  pod 'SAMKeychain'
  # OC版本ReactiveCocoa
  pod 'ReactiveObjC'
  # hook
  pod 'Aspects'
  # 键盘管理
  pod 'IQKeyboardManager'
  # 表格空视图
  pod 'DZNEmptyDataSet'
end

注释应该用#,不能用//,否则安装不了,有语法错误

  • 编辑完Podfile之后,执行pod install命令,下载第三方库,稍等一下就好,一般情况,源码没有多少的。

pod update也是可以的,除了可以安装新的,还可以升级旧的

  • XCodeworkspace, project, target三级管理项目。大多数情况,这三者用同一个名字。CocoaPods在默认情况下就是这样的,以后就打开xxx.xcworkspace文件就可了,第三方库,统一在Pods工程里,用户不用关心。
image.png
  • Pods目录是否要忽略,这里有争议。本人的观点是不需要忽略。整个项目中,由一个人进行管理CocoaPods,其他人只需要更新使用就可以了。

工程目录

image.png
  • 组件化:业务无关的组件,类似于后台的微服务。模块间不要相互调用。UI组件也是那种无状态的组件。按照功能划分。如果用到第三方库,也只是简单封装,增加一个隔离层。比如AFNetworking,封装后仅提供POST接口... ....

  • 服务化:按照业务进行隔离,类似于后台的服务。可以相互调用,也可以调用组件层的功能。是按照业务逻辑进行的一种封装。比如个人信息服务,网络服务,校验服务,等等

  • 模块化:这是基于页面的一种划分方式。可以将一个复杂页面划分成几个子页面,比如首页。也可将几个页面组合在一起,比如登录注册,实名认证等等。这个就相当于把一个APP拆成了好几个独立的小模块,降低复杂度。

  • 启动页,可以提供一个独立的故事版,单列出来

  • 基类:最大程序的复用,用一个单独的文件夹独立出来

  • 入口:比如main.storyboard,是程序的首页。

  • 图标:iOS10.3之后,系统提供API,可以切换程序icon

  • 至于public或者common文件夹,含义太笼统,不提供。

  • 常数:一些公用的常数定义,很多以前在public或者common中的内容可以放在这里:

image.png
  • 图片文件,界面文件等不提供专门的集中文件夹,而是跟随具体的使用场景,分散在合适的位置。

PCH文件

pch不是必须的,不过引入的话,可以带来很大的方便。这里是引入了,内容如下:

#ifndef WalletPrefixHeader_pch
#define WalletPrefixHeader_pch

// Include any system framework and library headers here that should be included in all compilation units.
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.

// 常数定义
#import "KJTConst.h"

// 基类
#import "KJTBase.h"

// 组件
#import "KJTKit.h"

// 服务
#import "KJTService.h"

#endif /* WalletPrefixHeader_pch */

这个文件最好像AFNetworking.h那样,作为顶级文件,不要包含具体的内容,作为一个隔离层,包含其他的头文件就可以了。比如,框架类的一些第三方库,就放在KJTThirdPart.h中。

#ifndef KJTThirdPart_h
#define KJTThirdPart_h

// 框架类的第三方库,直接引用,不适合添加隔离层
#import <MJRefresh/MJRefresh.h>
#import <ReactiveObjC/ReactiveObjC.h>
#import <Masonry/Masonry.h>

#endif /* KJTThirdPart_h */

Xcode中PCH文件的使用
Xcode中的基本设置(iOS9的数据加载问题,pch文件问题)

架构

  • 组件、服务这两块主要是为了复用,或者是隔离;一般情况下,很少涉及界面;就算是UI组件,也是没有状态的,一般是UIView的自定义子类。当然,如果确实与业务无关,也是可以引入界面的。比如扫描二维码,拍照片等等。

  • 模块是按照页面维度进行划分的。把整个应用放在一个main.storyboard种,很难看,一堆的warning。像以前的xib一样,固定一个,也不是很好。按照页面维度,按照关联性,拆成一小块一小块,感觉会好很多

  • 至于复杂页面,可以当做一个单独的模块,并且引入UIContainer,利用父子UIViewController的概念,进行进一步的拆分,可以很好的降低复杂度。比如首页模块。

  • UIViewController页面之间的传值,是一个比较麻烦的事情。比如,省市县的选择页面。展示页面和选择页面之间是强关联的,可以作为一个模块,放在一个storyboard中。页面之间的传值也是比较多的,操作起来比较麻烦,更好的做法是做一个单例,作用相当于全局变量。界面只是展示,用户的选择状态都放在单例之中统一管理。

  • 是否引入MVVM,根据个人喜好吧。引入的目的是为了给UIViewController减负,并不是替代,比如页面跳转,消息侦听等工作,还是UIViewController来做比较好。

  • 如果引入MVVM,那么ReactiveObjC.h这个第三方库最好用起来。当然,不需要信号系统,不需要函数编程,只需要很小的一块,就是UIViewControllerViewModel之间的双向绑定,用好RACRACObserve这两个宏就可以了。就像这篇文章介绍的那样iOS-ReactiveCocoa(RAC)的高级使用之视图与模型的双向绑定

  • 如果界面设计现出来,而接口设计迟迟没有出,那么就可以先开发界面,这个时候,引入MVVM就非常有必要了。先把页面写出来再说,并且属性名字按照自己的喜好来。等后台文档出来了,再对接一下就好了。

常量模块

  • 将一些工程通用的常量集中在一起,包括宏定义,枚举,宏定义等等
image.png
  • 强弱引用,log输出,一些方便方法等。
#ifndef KJTMacro_h
#define KJTMacro_h

///=============================================================================
/// 判断系统版本
///=============================================================================
#define kIosVersion      [[[UIDevice currentDevice] systemVersion] floatValue]
#define kIsIOS9          ([[[UIDevice currentDevice] systemVersion] intValue] == 9)
#define kIsIOS10         ([[[UIDevice currentDevice] systemVersion] intValue] == 10)
#define kIsIOS11         ([[[UIDevice currentDevice] systemVersion] intValue] == 11)
#define kIsIOS12         ([[[UIDevice currentDevice] systemVersion] intValue] == 12)
#define kIsAfterIOS9     ([[[UIDevice currentDevice] systemVersion] intValue] > 9)
#define kIsAfterIOS10    ([[[UIDevice currentDevice] systemVersion] intValue] > 10)
#define kIsAfterIOS11    ([[[UIDevice currentDevice] systemVersion] intValue] > 11)
#define kIsAfterIOS12    ([[[UIDevice currentDevice] systemVersion] intValue] > 12)

///=============================================================================
/// 一些常用的缩写
///=============================================================================
#define kApplication        [UIApplication sharedApplication]
#define kKeyWindow          [UIApplication sharedApplication].keyWindow
#define kAppDelegate        [UIApplication sharedApplication].delegate
#define kUserDefaults       [NSUserDefaults standardUserDefaults]
#define kNotificationCenter [NSNotificationCenter defaultCenter]

///=============================================================================
/// 一些常用的路径
///=============================================================================
// 获取temp
#define kTempPath           NSTemporaryDirectory()
// 获取沙盒 Document
#define kDocumentPath       [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]
// 获取沙盒 Cache
#define kCachePath          [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]

///=============================================================================
/// 判断数据是否为空
///=============================================================================
#define kStringIsEmpty(str)     ([str isKindOfClass:[NSNull class]] || str == nil || [str length] < 1 ? YES : NO )
#define kArrayIsEmpty(array)    (array == nil || [array isKindOfClass:[NSNull class]] || array.count == 0)
#define kDictIsEmpty(dic)       (dic == nil || [dic isKindOfClass:[NSNull class]] || dic.allKeys == 0)
#define kObjectIsEmpty(_object) (_object == nil \
|| [_object isKindOfClass:[NSNull class]] \
|| ([_object respondsToSelector:@selector(length)] && [(NSData *)_object length] == 0) \
|| ([_object respondsToSelector:@selector(count)] && [(NSArray *)_object count] == 0))

///=============================================================================
/// 强弱引用
///=============================================================================
#define kWeakSelf(type)         __weak typeof(type) weak##type = type;
#define kStrongSelf(type)       __strong typeof(type) type = weak##type;

///=============================================================================
/// 强弱引用
///=============================================================================
#define kDegreesToRadian(x)      (M_PI * (x) / 180.0)
#define kRadianToDegrees(radian) (radian * 180.0) / (M_PI)

///=============================================================================
/// 打印日志
///=============================================================================
#ifdef DEBUG
#define NSLog(...) NSLog(@"%s 第%d行 \n %@\n\n",__func__,__LINE__,[NSString stringWithFormat:__VA_ARGS__])
#else
#define NSLog(...)
#endif

#endif /* KJTMacro_h */
  • 颜色、字体、通知等内容较多,较集中,可以单独成为一个文件。比如,颜色:
#ifndef KJTColor_h
#define KJTColor_h

///=============================================================================
/// 颜色函数相关
///=============================================================================
// RGB
#define kRGBColor(r,g,b)             [UIColor colorWithRed:(r)/255.f green:(g)/255.f blue:(b)/255.f alpha:1.f]
// RGBA
#define kRGBAColor(r,g,b,a)          [UIColor colorWithRed:(r)/255.f green:(g)/255.f blue:(b)/255.f alpha:(a)]
// 随机颜色
#define kRandomColor                 [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0]
// 16进制颜色值,比如kColorWithHex(0xfdf4e1)
#define kColorWithHexA(rgbValue, a)  [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16)) / 255.0 green:((float)((rgbValue & 0xFF00) >> 8)) / 255.0 blue:((float)(rgbValue & 0xFF)) / 255.0 alpha:(a)]
#define kColorWithHex(rgbValue)      kColorWithHexA(rgbValue, 1.0)

///=============================================================================
/// 定义常用的颜色
///=============================================================================
// !! 蓝色(购买按钮、服务协议)
#define kBlueColor                      kColorWithHex(0x508CEE)
// !! 灰色(商品名、商户名标题)
#define kGrayColor                      kColorWithHex(0x868B90)
// !! 浅灰色(『同意协议』使用)
#define kGrayishColor                   kColorWithHex(0xA8ACAF)
// !! 黑色(商户名、商品名、银行卡四要素等使用)
#define kBlackColor                     kColorWithHex(0x3D4449)
// !! 红色(金额使用)
#define kRedColor                       kColorWithHex(0xFF6633)
// !! 分割线颜色
#define kLineColor                      kColorWithHex(0xE5E5E5)
#define kPageColor                      kColorWithHex(0xF5F5F9)

#endif /* KJTColor_h */

基类模块

是否引入基类,也是有争论的。比如,利用AOP,通过Hook的方式,来代替基类。相对来说,还是采用基类方便。当然,基类的功能要保持尽量简单,放在基类中的内容要严格控制。基类的目的是最大程度地复用

  • 引入友盟模块,进行页面时长统计,下面的函数只是对友盟相关接口的简单封装。
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    // 页面时长统计 开始
    [KJTUmeng beginLogPage:NSStringFromClass([self class])];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    // 页面时长统计 结束
    [KJTUmeng endLogPage:NSStringFromClass([self class])];
}
  • 设置页面背景色,默认的是白色,一般可以统一设置一下。
    // 设置背景色
    self.view.backgroundColor = kPageColor;
  • 统一添加返回按钮,第一个页面要除外:
#pragma mark - selector
- (void)leftBarItemAction:(id)sender {
    [self.navigationController popViewControllerAnimated:YES];
}

#pragma mark - private
- (void)setup {
    // 设置背景色
    self.view.backgroundColor = kPageColor;
    // 统一添加返回按钮
    if ([self.navigationController.viewControllers indexOfObject:self] != 0) {
        UIBarButtonItem * leftBarItem = [[UIBarButtonItem alloc] initWithImage:[[UIImage imageNamed:@"返回按钮"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] style:UIBarButtonItemStylePlain target:self action:@selector(leftBarItemAction:)];
        self.navigationItem.leftBarButtonItem = leftBarItem;
    }
}

不过这样设置返回按钮有一个问题,就是手势左滑功能没有了,感觉不是很好。

iOS控制器之基类设计

  • 后续的页面统一隐藏TabBar
    // 后续的页面都自动隐藏TabBar
    if ([self.navigationController.viewControllers indexOfObject:self] != 0) {
        self.hidesBottomBarWhenPushed = YES;
    }
  • 设置导航栏标题的颜色和字体
[self.navigationController.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor redColor],NSFontAttributeName:[UIFont systemFontOfSize:19.0]}];
  • 设置导航栏背景颜色
   // 设置导航栏背景颜色
    self.navigationController.navigationBar.barTintColor = [UIColor redColor];
  • 设置导航栏半透明属性translucentNO。不透明,能够解决很多布局问题。
    // 设置导航栏不透明;默认的半透明模糊效果会导致布局偏移
    self.navigationController.navigationBar.translucent = NO;
  • 设置返回按钮旁边的文字。默认是上一个页面的标题,感觉不是很好。统一给一个@“返回”,或者干脆给一个@“”。
// 设置返回按钮旁边的文字
    self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:(UIBarButtonItemStylePlain) target:nil action:nil];

参考文章

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

推荐阅读更多精彩内容

  • 3月10日上午9:00,焦化车间在文化宫一楼培训室召开三届二次职工大会。 做好签到会议签到登记 会前,车间领导为先...
    阮楠阅读 279评论 0 0
  • 李安域阅读 137评论 0 0
  • 花里佛陀,持续记录448天(2019.2.25) 阅读打卡第229天:《花间十六声》200-234;《复盘》210...
    和佛陀去赏花阅读 679评论 8 13
  • 新年和老朋友叙旧,地点是汉街上的某个甜品店。或是日子太苦了,我们都喜欢吃甜的。 一、旧友新愁 她在上海的某个知名的...
    成礼阅读 637评论 2 3