5 年 iOS 经验做的基础工程送给你,起步就领先别人

6,068 阅读14分钟
原文链接: ios.jobbole.com
原文出处: Thebloodelves(@IOS开发工程师在行动5366)   

写作原因:因为第一份工作有幸和5年iOS经验上司一起从头开始写项目(项目持续了半年),所以对于项目架构有点感悟,在这里献给大家(是自己写的项目,但是90%还原上司项目基础架构,数据库从CoreData换成了Realm,不过写法和功能甚至都一样),先给一个码云地址(github不知道抽什么风,提交不上去)https://git.oschina.net/liyongshi.com/BaseiOSProject.git;你们下载下载后可以直接以此开始写你们的项目(已经拿来写过三个公司项目了:快活、快照和帮帮管理助手(这个的源码在我的github地址上)),所以肯定是没问题的,常用的第三方、分类、工具、网络封装(基于AFN)和本地缓存(realm)等常用的都已经做好了(UtilKits.h中可查看);并且我已经把业务的前两层做好了(就是登陆、欢迎界面、首页转换逻辑,只供参考你们可以修改为你们的,第三步的需要自己跟着写下代码),你们可以按照这个思路来搭建页面和业务;这个系列总体分为两部分:介绍和使用

看前须知:1:里面的代码我是为了截图才故意写的很紧凑;2:如果你有一些自己的习惯在里面,比如类的nonull宏,那么你可以加上,基础工程代码很少,加上也不费时间;3:类前缀这个我没有加(之前加过,被上司叫重写了)实在抱歉;4:互相借鉴,重在参与;5:确实少了数据操作的线程管理,谢谢简友提醒

第一步:先瞄一下目录结构

111760865-0d63ce885a57d919

目录结构

UtilKits目录:顾名思义,这个目录用来存放分类、非pod导入的第三方库和设备信息等“辅助”元素,比如你还可以建立一个文件夹ProductInfo表示工程信息等等,然后全局文件(分类、AFN等)在UtilKits.h中导入;

AppDelegate目录:里面就是放了AppDelegate.h和AppDelegate.m文件,另起文件夹保留目录层级;

AppCustoms目录:自己写的轮子放到这里,比如图片多选等

ModelManager目录:存放模型、请求和模型管理,如上图中的Models中放模型,IdentityHttp是登录相关的请求(因为不用带入用户信息),IdentityManager操作缓存登录数据(读取、保存)和登录相关行为(退出);其实看UserManager更一目了然,但是太长了截图截不完,你们可以看看UserManager

InterfaceService目录:网路请求封装和地址宏

MainViewController目录:这里就是你的界面了,按照权限来存放,比如MainViewController管理登录(LoginController)、欢迎界面(WelcomeController)和登录后的界面(BusinessController),那么这三个文件夹就作为MainViewController目录的子目录,采用addChildViewController进行管理

好了,目录结构介绍完了,你们可以各取所需;比如你想要个定位管理器,那么你在和ModelManager同层目录建一个LocationManager即可,然后你有其他的分类分别放入对应的文件夹即可;当然了还有很多比如第三方登录那些,你自己在相应位置加上就可以了(因为不一定所有项目都有三方登录,但是所有工程都有这个基础工程中的内容,这里给的的不就是一个基础工程吗)

第二步:部分值得说的思想

1:本地数据库采用realm(你可能会问了,现在很多数据库啊,为啥这都要说),我们知道CoreData的强大是因为它有个NSFetchedResultsController和能直接存入对象(来自上司原话),存/取对象目前来说是基本要求了,那么你知道NSFetchedResultsController吗(不知道的去谷歌吧),然后我找了一下除了CoreData之外还有FMDB和realm有类似的库支持,但是FMDB不能直接存取对象我直接抛弃了,然后我找到了realm,它有一个专有库RBQFetchedResultsController来实现 CoreData的NSFetchedResultsController效果,这个非常有用,让我们可以轻松的做到界面实时显示最新数据(这才是我选realm的原因,前提是很难掌握CoreData);

2:本地数据存取每个用户是一个单独的文件夹,比如用户id为1的有一个名为1文件夹,id为2的有一个名为2的文件夹,然后公共的数据可以放到名为0的文件夹然后单独一个manager管理(因为usermanager是绑定了固定用户私有数据的);有的软件有游客身份,其实就是读取的默认文件夹0/default的数据(按照我这种思想),读取的时候可以看到UserManager中loadUserWithNo:函数就可以读取到对应用户数据了,而且这样做可以很容易迁移数据(我经历过旧数据迁移的疼,从最开始杜绝问题);

3:网络请求返回的数据和界面显示需要的数据不直接关联,他们和本地数据库直接关联,这是非常重要的一个思想,我们最开始的时候请求回调中直接把数据赋值给tableview的datasource,然后reloaddata还记得吗?这很容易造成显示错误,最明显的就是几个页面都对同一个对象数组进行操作会很难把控,所以我们正确的思路应该是:数据请求得到的数据我们直接存入数据库然后就可以结束了,数据库得到数据后发送一个回调(RBQFetchedResultsController的作用),然后所有RBQFetchedResultsController监听的页面重新从本地获取最新数据再赋值给tableview的datasource后reloaddata,这样的话请求就是请求没有其他的操作,数据变化监听由数据库特性发出;

4:权限(包括业务层次结构和manager提供的接口合理性等),我们在公司都是各司其职,不能越权,那么同样的软件也是有生命的(可以,很强势);至少我们不能犯这种低级错误:我们的软件基本都是有退出登录功能的,那么你至少不应该在退出登录点击事件执行self.window.rootViewcontroller = [LoginViewcontroller new],因为self.window.rootViewcontroller是固定的MainViewController,这个操作应该是发通知到MainViewController让它移除BusinessController(退出按钮肯定在这里面)并加载LoginController,这就是所谓的越权了;还有就是如果tableviewcell上的一个按钮点击后导航控制器要push到指定界面,你至少应该是tableviewcell的代理对象(一直到UIViewController)再push(虽然有人提出一个self-manager模式,但是我是怎么都不情愿那么做的)到指定界面;其他的还有很多,做之前想想应该是谁来做怎么做就可以了;

5:少用轮子尽量拆轮子,1:轮子大多数是综合考虑的,所以体积会很大而且里面的逻辑可能还会和你现有的逻辑冲突,比如解决键盘遮挡的IQKeyboardManager是检测键盘的弹起于消失然后使window的frame改变了,那么如果你自己对window有其他的操作就会冲突,2:用着是挺舒服的,但是你会慢慢的忘记很多知识,比如图片多选我们喜欢网上去找轮子但是又和自己的需求不一样于是又去找另外一个,其实自己做比找快多了其实我们都会做只是懒,其实你把轮子看懂自己拆分取其中一部分也是好的

第三步:做一个小demo展示界面显示实时刷新

其实大部分我们都写过,这里我只讲一下RBQFetchedResultsController怎么使用,RBQFetchedResultsController能解决什么问题?1:界面实时刷新,我们模型改变了通常是发一个通知然后要实时的界面接收通知,那么你有想过一个界面有很多模型的恐怖吗,而且前面也说了这种数据变化监听应该由数据库特性发出,2:和第一个有点异曲同工,也就是请求和显示分离,请求就是请求,不要去管显示,这样做有好处,也就是你可以在应用任何地方请求网络,其他所有地方都自动会刷新;好了下面我们来写个简单的demo,我们在AppDelegate.m中填充假数据:

121760865-8f5f92c3bf3e9437

填充假数据

然后我们在BusinessController文件夹中创建一控制器FirstController(带xib,为了方便),在中间展示当前用户的名字,下面放一个输入框,输入框下边一个按钮点击后修改用户名字为输入框的内容,然后创建一个RBQFetchedResultsController代理设置成自己以便获取最新的用户信息,导航右边按钮不断创建自身并push以测试效果,界面和代码如下:

131760865-cf90ad35864c863e

demo界面 141760865-2a3841bd1dad01c5

FirstController.m上面代码 151760865-cb2fce3d897d0c92

FirstController.m下面代码

上面的RBQFetchedResultsControllerDelegate基本还原CoreData的FetchedResultsControllerDelegate(数据库操作会触发这些回调,很强势),然后我们BusinessController(登录后的界面)中创建一个导航视图,rootviewcontroller设置成FirstController,代码就像这样:

161760865-c73b36d35c6eb060

BusinessController.m代码

然后我们启动模拟器,多点下右上角的“+”号创建几个FirstController实例,然后我们输入内容点击按钮,你再退回来看看是不是所有界面都变了?效果如下:

171760865-078ea8a40fb2cf38

效果图

这部分的代码工程里面没有,你们跟着写一下实践一下;好了,你现在可以拿着这个基础工程开始你自己的项目了,对了realm有几个坑,我在https://github.com/TheBloodElf/iOSDevNotices/issues中记录了一些,如果你遇到了你可以先去看看;最后,希望这个基础工程可以为你减少项目开发的时间

第四步:做一个小demo展示请求与显示分离

这篇文章出来后得到很多人的关注,受宠若惊,当然也有喷这工程没有5年沉淀的;我在这里统一说明一下这只是一个基础工程,是为了让更多人拿来即用的;为了满足一些人的要求,我就写一点这个基础工程稍微能体现沉淀的例子吧;这一步我们就来写真实接入顺便讲讲realm怎么使用,首先去前面的码云地址下载项目,然后先编译一下确保没有问题,昨天我看到回复中有人编译出错了,我一看提示大概是这两个地方,大家注意了,这两个地方尽量一样,然后还出错就clean项目、重启模拟器、重启XCode、重启电脑……:

181760865-0469a067e52d1d5b

项目最低适配版本 191760865-6562da8457e8ac0d

不知道什么意思

为了还原真实开发环境,我定义(YY)了一个接口(我用新浪云php做了个后台,想了解后台的可以看我相应的文章)文档(只有一个api,用于演示)如下:

201760865-4c46d2a983e0529a

接口文档

php后台添加假数据(没有读取数据库)代码(这里没有处理参数userNo):

211760865-ce2123c9d56950ea

后台代码

然后我们在浏览器地址栏中输入http://haiphp.applinzi.com/user/index.php,就能看到这个效果了:

221760865-cc5d2416286c94d4

请求成功

那么这个怎么接入我们的基础工程呢,首先在这里把地址换成文档中的基础地址:

231760865-f875c66d8b4368ad

1:换地址

然后确认解析响应是否正确,我们的code为0表示成功,返回字段和文档一样:

241760865-45101007ff3661ac

2:确认解析响应逻辑

然后就是创建一个模型和文档返回数据中data一样:

251760865-6d3f1922ab0dccba

3:创建模型 261760865-b7d236660a8d9ae9

4:设置主键

我们这里假设用户已经登录了,所以我们依然在项目入口出写上假用户数据:

271760865-4b3f9a226cb00664

5:写上假当前用户数据

当然我们要对UserFriend做本地缓存和表监听,在用户模型管理器中写好接口(这里我只写了三个,你们可以自己加):

281760865-e9ec64200bb9bef6

6:管理器中写好接口 291760865-bb9450c9ee849de5

7:管理器中写好实现

好了,本地缓就做完了,我们偷个懒,直接用这个控制器(真实项目用addChlidController加一层再做其他的,为了演示这里就稍微放纵一点)来演示吧:

301760865-44494e7b4cb05088

8:选择要演示的控制器

老规矩,我们先创建一个表格视图用来演示,代码我这里先截一个最基本的,后面的步骤我就不全屏截图了,只截部分代码:

311760865-98dc39429ac8f912

9:创建表格视图用来展示

上面有一个警告说的是:

321760865-d7dc8b8b95a2a494

RBQFetchedResultsControllerDelegate警告

这个不用管,因为这些回调我们是选择性的使用;如果你有警告强迫症(上家公司项目中没有一个警告,这家公司不行,因为很多三方库),那么去RBQFetchedResultsController源码改一下就行了:

331760865-c520e715746ff138

解决警告

现在我们运行一下应该就能看到一个空白的表格视图了:

341760865-19eba1a99d9c5326

10:跑一下看到效果

好了,现在开始写展示代码了,既然做了本地缓存,那么我们在控制器初始化的时候当然要先读取本地数据,所以我们再创建一个UserManager对象,用来取数据:

351760865-e79046ba38a6a4f6

11:取得本地数据

然后我们要把数据监听写上,就像这样:

361760865-442c119f7516e121

12:加上数据监听

对了,请求怎么能忘记了,我们在这里写上请求接口:

371760865-49083f4e1636a954

13:写上请求接口 381760865-640d117107bb99fd

14:写上请求实现

好了,现在我们还差请求数据了,我们创建一个按钮,响应代码就像这样:

391760865-10a3f581c35ca4cf

15:请求代码

这时候我们可以运行起来了,然后点击按钮就能看到这样的效果了:

401760865-629e92f3380c9ffb

16:请求成功

好了,大家可以看看15步中的请求,我们请求完没有去管界面显示的事情,也就是没有这样写:

411760865-d8d27e1be8417e8d

请求和显示绑定了

用realm+RBQFetchedResultsController就能达到分离了,甚至15步这段请求代码可以放到项目任何一个地方(控制器瘦身?),只要数据改变这个控制器自动就会去刷新了(我有深刻体会);好了先更新到这里,如果有什么需要了解的请回复在下面,我会持续更新这篇文章

第五步:realm解决多线程操作同一对象混乱问题

相信大家在使用realm时被各种崩溃弄个狗血淋头,我这里大概说一下最重要的两点

1:直接从realm读出来的对象不能直接修改

其实就是通过objectsInRealm读出来的对象不能直接改变属性,如果你要改变那么请你自己重新copy一份

2:只有直接从realm读出来的对象才能被删除

也就是调用deleteObject的参数对象必须是从objectsInRealm直接读出来的对象

3:realm可以异步操作,网上很多说只能主线程操作是错误的

我文章下面的回复我也说错了,不好意思;而且文章开头也提出来了,我工程没有加线程操作部分,实在抱歉,你们可以自己加上

可能现在有人比较蒙蔽了,我怎么能控制好呢?我当然需要删除或者修改,那我怎么知道当前是copy还是直接读出来的呢?其实我总结了一个方法,现在你的程序就不会在realm部分崩溃了(我也是使用了两个月后才发现这样一个解决方法,可能网上还有其他的方法吧)

1:读出数据后直接copy

也就是我们从objectsInRealm读出后直接copy,这样修改没问题了,代码就像这样:

421760865-0a7ff2afe82556c0

直接copy读出来的对象

那么有人可能会说了,这样我删除怎么办?

2:删除时再次读取

431760865-a4a1740ca350572b

删除时再读取一次

因为对象有唯一标示,所以这样再查一次也不费时间,而且这样做了后你的增删改查都不会崩溃了;通过以上的两点解决方法我们就可以完美的解决多线程操作同一对象混乱的问题了。