0版iOS客户端重写中的一些经验,完全解析Androi

地铁通是一款覆盖了本国全部和国外一些已开展大巴城市的导航类应用,入选了苹果的AppStore杰出,至今仍在大巴首要字寻觅排行第一。迄今截止(注:二零一五年4月)iOS版的总下载数为237万,苹果后台展现的日活跃顾客约为5万。

原文: 

参照他事他说加以考察博客:Alamofire Tutorial: Getting Started

应接Follow小编的GitHub, 关怀笔者的简书, 博客目录.

本人是从贰零壹叁年起接替的这些动用,在那此前是一个在神州安家落户的高卢雄鸡同事凯文搭起的总体架构,在SVN呈现的率先次提交是二零一一年5月一日。在那长时间的年月里,由于能够想像的类型进程和迭代压力,重写架构这种劳苦不讨好的事情自然一贯没办法计划。

 

Alamofire是Swift的HTTP网络工具包,也就是Swift完成AFNetworking版本。通过二个称作PhotoTagger的德姆o,你将学会怎么样选择Alamofire进行基本互连网操作。德姆o中,你将上传图片到第三方服务Imagga API,并让Imagga API为你的图片加上标签和配色。

图片 1MVC

其实,小范围的重构一贯在开展,在2015年11月的一个本子作者是因为花了异常的大气力重构得比较狠,还恶搞苹果的Jony Ive,用他的语气做了几页宣传PPT:

自身几周前写过一篇作品,叫《被误解的 MVC 和被神化的 MVVM》,当中的过多切磋是和本文的小编Lancy 交换获得的。当时无数人复苏问:能一向上猿题库的代码吗?此次 Lancy 的那篇小说就径直上代码了。

用Cocoapods导入Alamofire的时候,Terminal显示成功,但是当自家import Alamofire的时候,出现了Cannot load underlying module for 'Alamofire'的标题。一最早自己感到是cocoapods版本难点,查看版本是对的;笔者又如约Cannot load underlying module for 'Alamofire' · Issue #441 · Alamofire/Alamofire · GitHub上说的,重新导入Alamofire,照旧不对。最后,发掘是贰个非常粗略的荒谬。cmd B就足以了import Alamofire了。

本文的合集已经创作成书,高等Android开垦深化实战,迎接各位读友的提商谈引导。在京东即可购买:

图片 2图片 3图片 4图片 5图片 6图片 7图片 8

那篇文章详细介绍了猿题库顾客端框架结构的设计和思虑,当然,也可以有恢宏的代码示例。Lancy 引进了一个名称叫 Data Controller 的层级为 View Controller 消肉,况兼借鉴了 MVVM 的企图来将分界面与底层解耦。

simulator

图片 9Android

即便,更加大局面,只怕说,更底层的框架结构级重构照旧直接没时间开展。由于横跨多少个业务场景,这种等级的重构职业一定占用几周的完好时间,还要实行深透的测量试验。作为一家创办实业公司是承受不起的,只可以想想。

那套架构支持猿题库通透到底解耦了UI和逻辑层的支出专门的学业,何况使 View Controller 的代码极为精简,由于 Data Controller 与分界面无关,它仍然使单元测量试验和 TDD 成为也许。

Demo地址:GitHub - Paganarchitect/iOS_tutorial_exercises_RayWenderlich: iOS exercises based on Ray Wenderlich tutorial

Android开辟已经日臻成熟, 在此在此以前一直困扰的花色架构难点, 也随着社区的不断努力, 从MVC中碰到启迪, 陆陆续续推出MVP, MVVM等架构模型, 进而创制适合Android的付出框架. 未采用架构的项目:

正如Pinterest在介绍他们重写程序架构的blog(《Re-architecting Pinterest's iOS app》)里面写的:

 

#1 安装Alamofire

开发Podfile,将里面代码替换来如下。Alamofire前边能够不要跟版本号,pod本身会清楚那是Alamofire的流行版本的。

Podfile

在巅峰里:pod install

设置成功

  1. 未分离分界面逻辑与作业逻辑;
  2. 本子迭代导致全体相关类, 不断供给修改;
  3. 模块高耦合低内聚, 存在遮盖Bug, 无法编码测量试验;

A small team of Pinterest iOS engineers was recently given the opportunity every engineer dreams of - completely rethinking and rebuilding our app.

唯独,好的架构都与具象的事体场景相关,希望大家在攻读的时候也能尽量知晓它的试用场景,最后能够改动和煦APP的架构。

#2 Alamofire为主用法

1.出殡和埋葬为主央浼

出殡最核心的GET央浼

为呼吁增多参数,管理响应音讯

responseJSON是从服务端再次来到的JSON数据,除却,Alamofire还接济一般数据,字符串和plist方式的回来。

2.上传文件

上传文件到服务器

获得上传进程

帮忙multipartFormData格局的表单数据上传

3.下载文件

下载文件

安装暗中认可的下载地点

检验下载进度

4.HTTP验证

authenticate方法

5.HTTP响应状态音信识别

手动识别

自动识别。自动以为200..<300的statusCode平时

6.调节和测量试验情状

打印诉求的详细消息

行使框架结构的对象正是为了消除那么些标题. 全体架构都出自初期的MVC, 即Model-View-Controller模型, 本文由近及远剖判MVC, MVP, MVVM三类主流架构方式, 并解释在那之中的优劣势.

以致自个儿调节要离职,出于一种代码洁癖和有限的社会权利感(作者觉着在作者从此怕是没人能做得起这种量级的代码调解了,难道就把这一坨五年历史的满载补丁的垃圾堆留给200多万客户吗?),外加一股想要自己救赎的扼腕(作者觉着,能友好把自个儿的烂代码改掉是一种本人救赎),小编向Boss提议了重构申请,作为离职前的结尾七个品类。

可望能给我们帮忙。

#3 REST, JSON,HTTP简介

1.HTTP:是一种互连网传输公约。它定义了那二种央求方法:

--GET用于获取数据

--HEAD类似GET,但只可以获得header,不可能收获实际多少

--POST将数据发送到服务器

--PUT将数据发送到多少个具体地点

--DELETE将数据从一个具体地方删除

2.REST全称是REpresentational State Transfer,指的是一组架构约束标准和条件。借使二个架构符合REST的束缚原则和原则,大家称它为RESTful框架结构。那几个准则包蕴:不保险伏乞状态,缓存需要,提供特殊的接口。那使得app开采者集成API时绝不追踪央浼的数据状态。

3.JSON完备是Javascript Object Notation,是一种轻量级的数据调换语言,以文字为底蕴,易于阅读。它的数据类型有限。Apple提供NSJSONSerialization类来将对象转化为JSON等格式

多少个架构的剖判均已形成, 参谋MVC, MVP, MVVM.

大巴通4.0项目就这么立项了。

小编介绍

蓝晨钰(@晨钰lancy),iOS 开垦者,现居上海,就职于猿题库,关怀代码品质,团队频率和制品体验,博客: 。

多谢蓝晨钰授权发表,本文的兼具打赏归蓝晨钰全数。

#4 为啥要选拔Alamofire

Alamofire是以NSUTucsonLSession为根基的,可是比较NSUPAJEROLSession,它更简约,逻辑更清楚。Alamofire有这么些关键效用:上传,下载,供给。这几个功效的成效域是module,并非class和struct。

MVC, 即Model-View-Controller, 基于页面逻辑的退换要多于业务逻辑, 分离三种逻辑收缩类代码的修改.

在深远讲明以前,作者先全部介绍一下大巴通那款app的大约组成。

猿题库是一个兼有数千万顾客的创办实业企业,从二〇〇三3年题库项目运行到贰零壹伍年,团队保持了相当高的生产作用,使大家的成品造成了几个大版本和数11个小本子的短平快迭代。

在如此急忙的付出进程中,如何保管代码的质量,降低中期维护的资金,以及为项目更加快的版本迭代速度提供支撑,成为了我们关怀的第一难题。那篇小说将表达我们在猿题库 iOS 顾客端的架构划虚拟计。

#5 上传文件

1.格式转变。保障Imagga API获得正确的格式。

ViewController.swift

2.因为Alamofire是异步的,所以您的UI更新也要以异步的点子。

ViewController.swift

3.导入Alamofire,并调用upload方法。

找到uploadImage方法,并加多以下代码.思路是:

上传成功--总括上传进度--检查response是还是不是成功--检查response的每一有些,确定保证项目正确--上传实现,清空数组

上传失利--输出encodingError

ViewControllers.swift

  • Model: 即数据层, 担负处监护人务逻辑, 监听网络与数据库接口.
  • View: 即分界面层, 突显来源于Model的数据.
  • Contoller: 即逻辑层, 传递客商的互动和更新Model的数据.

图片 10

MVC

MVC,Model-View-Controller,我们从那些古老而卓绝的设计格局出手。选拔 MVC 这些架构的最大的长处在于其定义轻易,易于了然,差不离任何八个程序员都会具备明白,大致每一所计算机械学学院都教过有关的文化。而在 iOS 客商端开垦中,MVC 作为法定推荐的主流架构,不但 SDK 已经为大家兑现好了 UIView、UIViewController 等连锁的组件,更是有恢宏的文书档案和模范供大家参谋学习,能够说是一种特别通用而干练的架构设计。

但 MVC 也许有她的坏处。由于 MVC 的概念过于简短朴素,已经特别难以适应最近顾客端的须要,大批量的代码逻辑在 MVC 中并不曾概念得很了然终归应该投身什么地点,导致她们很轻巧就能够堆放在 Controller 里,成为了人人所说的 Massive View Controller。

#6 获取数据

1.获取tags

获取tags

2.获取colors

获取colors

3.更新upload()的completion handler方法

更新completion handler方法

完成!!!

图片 11MVC

如上面包车型地铁主分界面截图,大巴通的首要意义便是呈现大巴图和询问四个站点间的一级换乘路线,大约的模块划分如下:

MVVM

MVVM,Model-View-ViewModel,一个从 MVC 格局中前行而来的设计方式,最先于二零零六年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提议。在 iOS 开拓中试行 MVVM 的话,日常会把大批量原先坐落 ViewController 里的视图逻辑和数据逻辑移到 ViewModel 里,进而使得的缓慢解决了 ViewController 的担任。

别的通过分离出来的 ViewModel 得到了越来越好的测验性,我们能够针对 ViewModel 来测量检验,化解了分界面成分难于测验的题目。MVVM 常常还有只怕会和一个强有力的绑定机制一起专门的工作,一旦 ViewModel 所对应的 Model 发生变化时,ViewModel 的习性也会产生变化,而相呼应的 View 也跟着发生变化。

一直以来的,MVVM 也可以有她的宿疾:

一个要害的老毛病是,MVVM 的就学开销和开垦开销都相当高。MVVM 是一个年青的设计情势,大大多人对她的垂询都比不上 MVC 熟稔,基于绑定机制来实行编制程序必要自然的读书技巧较好的左侧。同不时候在 iOS 客商端开采中,并不曾现有的绑定机制能够运用,要么使用 KVO,要么引入类似 ReactiveCocoa 那样的第三方库,使得学习话费和开辟开销进一步升高。

另三个毛病是,数据绑定使 Debug 变得更难了。数据绑定使程序非常能异常快的传递到其余岗位,在分界面上开掘的 Bug 有异常的大只怕是由 ViewModel 形成的,也可以有希望是由 Model 层造成的,传递链越长,对 Bug 的永世就越困难。

再便是还非得提出的是,在古板的 MVVM 架构中,ViewModel 依然承载的汪洋的逻辑,包涵业务逻辑,分界面逻辑,数据存款和储蓄和网络有关,使得 ViewModel 依然有非常大可能率变得和 MVC 中 ViewController 同样臃肿。

基于MVC架构, View和Controller都会借助于Model, View显示Model数据, Controller更新Model数据. Model从种类中分别后, 独立于UI, 允许测量检验. 更新Model情势的比不上, 把MVC架构分为被动模式和主动模式.

图片 12

在二种架构中权衡而产生的架构

三种架构的帮助和益处都想要,劣势又都想逃避,大家在两种架构中权衡了他们的得失,设计出了一个新的框架结构,起了叁个名字叫:MVVM without Binding with DataController,架构图如下:

图片 13

在被动方式中, Controller是独一操作Model的类. 基于客户的响应事件, Controller通告Model更新数据. 在Model更新后, Controller公告View更新UI, View从Model中猎取数据.

接下去,让大家剖析一下在3.x版本的客车通里面都设有何样必要改良的难点。

ViewModel

先来看右侧视图相关的片段,古板的 MVC 个中 ViewController 中有大气的多少呈现和体制订制的逻辑,我们引进 MVVM 中 ViewModel 的定义,将那有的视图逻辑移到了 ViewModel 其中。

在那一个规划中,每一个 View 都会有三个对应的 ViewModel,其蕴藉了这几个 View 数据展现和体裁定制所必要的享有数据。同时,大家不引进双向绑定机制依旧观望机制,而是经过守旧的代办回调或是通知来将 UI 事件传递给外部。而 ViewController 只须要生成四个 ViewModel 并把那么些装配给相应的 View,并接受相应的 UI 事件就能够。

那般做有多少个好处:首先是 View 的一点一滴解耦合,对于 View 来讲,只必要鲜明好相应的 ViewModel 和 UI 事件的回调接口就可以与 Model 层完全割裂;而 ViewController 能够制止与 View 的具体表现打交道,那某些任务被转送给了 ViewModel,有效的缓解了 ViewController 的肩负;同期大家弃用了古板绑定机制,使用了理念的轻巧明白的回调机制来传递 UI 事件,减少了上学习费用用,同不时候使得数据的流入和流出变得轻巧观望和垄断(monopoly),收缩了维护了调适的资金财产。

图片 14Passive MVC

ViewController职务过多

那足以说是其他贰个未精心设计过架构的iOS App必然会导致的结果,在苹果的MVC架构中,ViewController这一流是最轻便膨胀的,产业界汹涌澎拜的诸如MVVM等架构便是为了消除这种难题。

切切实实到项目中,比如在突显搜索结果的线路结果页中,当线路数量传到那么些页面时,须求重新加工才具获取该页面所急需的音信(例如哪个站是换乘站、一共须求换乘三回、总结合理的末班车时间等),而那几个专门的学问总体放置了脚下ViewController中张开。再加多别的一些管理逻辑,导致那些ViewController变得极其臃肿,到达了天怒人怨的2000多行。

再比如切换差别城市大巴的城市列表页中,由于并未有办好方便的分层,导致那些ViewController除了出示城市列表的职责之外,还担任了:

  • 点选切换城市之后的数量起首化工作;
  • 点选开首下载后的速度更新逻辑,以致下载达成后的解压逻辑;
  • 点选删除城市之后的数量清理职业;
  • ...

DataController

接下去大家关注 Model 和 VC 之间的关系。如以前提到,在价值观的 MVVM 中,ViewModel 接管了 ViewController 的大相当多职分,包括数据获得,管理,加工等等,导致其很有望变得臃肿。大家将那有的逻辑抽离出来,引进贰个新的预制构件,DataController。

ViewController 能够向 DataController 央求获取或是操作数据,也能够将一部分风浪传递给 DataController,那一个事件能够是 UI 事件触发的。DataController 在接收那些诉求后,再向 Model 层获取或是更新数据,最终再将赢得的数据加工成 ViewController 最后须求的数量再次来到。

与上述同类做之后,使得数据相关的逻辑解耦合,数据的拿走、修改、加工都位居 Data Controller 中拍卖,View Controller 不关注数据怎么样获得,如何管理,Data Controller 也不爱抚分界面怎样呈现,怎么着互相。同期 Data Controller 因为一心和分界面毫不相关,所以能够有更加好的测验性和复用性。

DataController 层和 Model 层之间的底限而不是顽固的,但要求确认保证每一个ViewController 都有贰个对应的 DataController。Data Controller 更强调的是其用作工作逻辑对外的接口。而在 DataController 中调用更底层的 Model 层逻辑是我们推荐的编制程序范式,举个例子数据加工层,网络层,悠久层等。

在背后的例子中,大家会更详实的教师 DataController 的贯彻细节。

积极格局中, Controller不是独一操作Model的类, Model存在自更新机制. 在创新数据时, Model层使用阅览者情势公告View和其余类. View实现观望者的接口, 在Model中, 注册改成旁观者, 接收通告.

耦合过紧

从上边的结构模块示意图中可以见到,在差别模块之间存在着千头万绪的强正视,当中尤以DataSource极致惨恻,因为在全路App运维期间都要保持分化界面包车型地铁某种数据一致性。固然在此前的架构中特地抽象出了DataSource那些单例,但各种模块间都以平昔对其打开操作,不止繁琐、轻易失误,从架构的角度来看,有些底层的操作完全不应有由最上层的ViewController来做。

再举个例证,在3.x本子的地形图控件中(即上海教室的TileView),存在着大批量甩卖和客商交互的代码逻辑,以致还大概有客商点选起源和顶峰后直接对DataSource拓宽更新的逻辑。过紧的耦合导致了不供给的推断,到近日本人都无法完全搞清前边同事写的上面这几行代码中,那八个玄妙的数字到底是干嘛的:

 NSInteger tag = -1; if ([sender isKindOfClass:[UIButton class]]) { UIButton *tmpBtn = (UIButton *)sender; tag = tmpBtn.tag; } if (sender == bubbleView.startBtn || tag == 888 || tag == 887) { // WTF?? ... }

Show me the code

大家以猿题库主页为例,呈现我们是什么样运用使用那一个框架结构的。

图片 15

主页有多少个部分构成,最下面的小猴子 Banner 页,用于滚动浮现一些运动音信;中间有多少个顾客名字的页面,用于展现客户消息和答题情状以及部分心灵鸡汤;最下边包车型大巴那部分是多个课目选择页面,呈现了顾客展开的科目入口,在越来越多选拔里面能够更上一层楼安顿这几个学科入口。接下来我们会以学科页面(SubjectView)为例体现一些细节。

图片 16Observer MVC

产品逻辑混乱

那条看起来也是有一点点甩锅的情趣,但实质上境况的确那样:假若有哪条逻辑写起来非常拧巴,恐怕连描述起来都拧巴的时候,你能够去认真思虑一下,是还是不是从产品设计上就出了难点。

固然从流水生产线上讲,这种上溯应该幸免在急需评定调查之类的机缘,但从实际上奉行的角度,已经实现的作用并不表示便是不行改造的。特别在意识那部分代码已经变为维护鬼世界的时候,应该果断进行改良。

举例在书签/历史线路页面,从前产品的须求是询问过的站点和路径定时间逆序混杂在联合展示,那样变成的结果正是,在布局那一个页面时要求多量的动态判别逻辑来差距各种cell到底是怎么东西:

 NSObject *obj = [historyMArray objectAtIndex:row]; if ([obj isKindOfClass:[NSDictionary class]]) { ... }else if ([obj isKindOfClass:[Route class]]){ ... }else if ([obj isKindOfClass:[NSArray class]]){ ... } return cell;

而只要把页面分为若干个section突显,不仅仅从根源上杜绝了这种纷乱的逻辑,从成品的角度也使顾客一眼看上去特别清晰。

ViewController

咱俩会给每一个 ViewController 都成立多个相应的 DataController。
比如大家给主页建一个类起名为APEHomePraticeViewController,相同的时间他会有二个应和的 DataController 起名字为 APEHomePraticeDataController。同时大家把页面拆分为多少个部分,各个部分有二个相对应的 SubView。代码如下:

@interface APEHomePracticeViewController () <APEHomePracticeSubjectsViewDelegate>

@property (nonatomic, strong, nullable) UIScrollView *contentView;
@property (nonatomic, strong, nullable) APEHomePracticeBannerView *bannerView;
@property (nonatomic, strong, nullable) APEHomePracticeActivityView *activityView;
@property (nonatomic, strong, nullable) APEHomePracticeSubjectsView *subjectsView;

@property (nonatomic, strong, nullable) APEHomePracticeDataController *dataController;

@end

viewDidLoad 的时候,开头化好种种 SubView,并设置好布局:

- (void)setupContentView {
    self.contentView = [[UIScrollView alloc] init];
    [self.view addSubview:self.contentView];
    self.bannerView = [[APEHomePracticeBannerView alloc] init];
    self.activityView = [[APEHomePracticeActivityView alloc] init];
    self.subjectsView = [[APEHomePracticeSubjectsView alloc] init];
    self.subjectsView.delegate = self;
    [self.contentView addSubview:self.bannerView];
    [self.contentView addSubview:self.activityView];
    [self.contentView addSubview:self.subjectsView];
    // Layout Views ...
}

接下去,ViewController 会向 DataController 乞求 Subject 相关的多少,并在伸手实现后,用获得的多寡生成 ViewModel,将其装配给 SubjectView,达成分界面渲染,代码如下:

- (void)fetchSubjectData {
    [self.dataController requestSubjectDataWithCallback:^(NSError *error) {
        if (error == nil) {
            [self renderSubjectView];
        }
    }];
}
- (void)renderSubjectView {
    APEHomePracticeSubjectsViewModel *viewModel =
        [APEHomePracticeSubjectsViewModel viewModelWithSubjects:self.dataController.openSubjects];
    [self.subjectsView bindDataWithViewModel:viewModel];
}

当Model层产生多少更新时, 告知全部观看者. View从Model中立异数据.

页面布局格局过时

唯恐对此大繁多现成的App来讲,这已经不算是个难点,但对此一个持有几年历史的App,基本上最初的那个分界面依旧是用相对坐标举办固化的;加载一个动态变化的UI时,也是一丢丢乘除后再addSubview上去,或然涂改有些控件的frame.size.height来更新其入骨。这种做法不止不优雅,更要紧的是不直观。一切不直观的代码都会促成以下几点难点:

  • 支出时难以调节和测验;
  • 并发bug时难以维护;
  • 得到新须求时麻烦修改。

所谓的KISS原则(Keep It Simple & Stupid),指的正是这么的气象。

接头了现存的难题,上面要做的就是局地客观的规划了。

有了下边所述的教训,在计划新版架构的时候,从最先先就创设了几条原则:

  1. 尽恐怕打垮耦合,只在有不可缺少的情事下利用紧耦合;
  2. 牢记合理封装,每种模块严谨只负担本人的工作;
  3. 力争维持简洁,碰到复杂的完结换用轻松的统一计划。

下边是本身当下在日记本上画的下载模块的档次结构暗中提示图:

图片 17

每一层之间仅保留最宗旨的数额乞求和数量报告。

新的组织图差十分少是其同样子:

图片 18

最刚烈的分别正是把模块之间的紧耦合形成了虚线展现的Notification,并且去掉了具备跨级调用。

在起来项目事先,二个要做的采纳是,要用原有的Objective-C照旧后来的斯维夫特?

终极本人选拔了斯威夫特。理由是:

  1. 斯威夫特经过几年的发展,已经趋于稳固;
  2. 本次重写的目的正是要面向以后,而斯威夫特正是未来;
  3. 借机学习新的技能。

接下来,小编用"斯维夫特 best practices"作为关键词寻觅实行了一番应用商讨,尽量从最发轫就站在有影响的人的肩膀上,往准确的方向走。

诸如那篇:

有的在项目中采用的点:

数据结构

为了更加好的自己要作为典范遵守规则,大家接下去要介绍一下 Subject 相关的数据结构:
APESubject 是学科的财富结构,富含了 Subject 的 id 和 name 等能源属性,这一部分质量是客商无关的;APEUserSubject 是顾客的教程消息,富含了顾客是还是不是张开有个别学科的属性。

@interface APESubject : NSObject
@property (nonatomic, strong, nullable) NSNumber *id;
@property (nonatomic, strong, nullable) NSString *name;
@end
@interface APEUserSubject : NSObject
@property (nonatomic, strong, nullable) NSNumber *id;
@property (nonatomic, strong, nullable) NSNumber *updatedTime;
///  On or Off
@property (nonatomic) APEUserSubjectStatus status;
@end

图片 19Active MVC

使用struct来定义常量

在Objective-C时代平常用#define来定义常量,以后能够动用struct来定义。比如:swift struct CMConfig { static let AppName: String = "xxx" static let AppStoreID : UInt = 8888 static let WeiboKey: String = "8888" static let WeiboAppSecret: String = "8888" static let AppStoreURL: NSURL = NSURL(string: "itms-apps://itunes.apple.com/cn/app/bei-jing-de-tie-touchchina/id530096786")! ... }诸如此比能够最大程度地使用斯维夫特种类安全的语言特色,使您在编写翻译期就足以检查出有个别低档的失误。

DataController

如大家事先所说,每贰个 ViewController 都会有贰个一见倾心的 DataController,这一类 DataController 的首要任务是管理这些页面上的享有数据相关的逻辑,大家称其为 View Related Data Controller。

// APEHomePracticeDataController.h
@interface APEHomePracticeDataController : APEBaseDataController
// 1
@property (nonatomic, strong, nonnull, readonly) NSArray<APESubject *> *openSubjects;
// 2
- (void)requestSubjectDataWithCallback:(nonnull APECompletionCallback)callback;
@end

上边的那一个代码

  1. 我们定义了三个分界面最终必要的数码的 property,这里是 openSubjects,那个 property 会存款和储蓄顾客展开的课程列表,他的体系是APESubject
  2. 我们还可能会定义二个接口来呼吁 openSubject 数据。
    DataController 这一层是多个回船转舵极高的预制构件,三个 DataController 能够复用越来越小的 DataController,这一类越来越小的 DataController 经常只会含有纯粹的只怕更抽象的 Model 相关的逻辑,举个例子网络央浼,数据库须求,或是数据加工等。大家称这一类 DataController 为 Model Related Data Controller。
    Model Related Data Controller 常常会为上层提供正交的数额:
// APEHomePracticeDataController.m
@interface APEHomePracticeDataController ()
@property (nonatomic, strong, nonnull) APESubjectDataController *subjectDataController;
@end
@implementation APEHomePracticeDataController
- (void)requestSubjectDataWithCallback:(nonnull APECompletionCallback)callback {
    APEDataCallback dataCallback = ^(NSError *error, id data) {
        callback(error);
    };
    [self.subjectDataController requestAllSubjectsWithCallback:dataCallback];
    [self.subjectDataController requestUserSubjectsWithCallback:dataCallback];
}
- (nonnull NSArray<APESubject *> *)openSubjects {
    return self.subjectDataController.openSubjectsWithCurrentPhase ?: @[];
}
@end

在大家的 APEHomePraticeDataController 的兑现中,就带有了三个 APESubjectDataController,这个 subjectDataController 会担当央浼 All Subjects 和 User Subjects,并将其加工成上层所最后必要的 Open Subjects。(备注:那个例子里面包车型地铁 callback 会回调多次是猿题库产品的须求,如有必要,可在这一层调节央求都成功后再调用上层回调)
实际,Model Related Data Controller 能够常备的以为正是我们日常在写的 Model 层代码,比方 UserAgent,UserService,Post瑟维斯之类的劳务。之后读者若想重构就项目成那一个架构,大能够没有须求纠结于情势,直接在 DataController 里调用旧有代码的逻辑就能够,如图下边那样的表现都以同意的:

图片 20

在开始时期开辟中, Activity(或Fragment)既是View又是Controller, 并未有举行分离. 某些系列剥离出Model, 独立于阳台开展测量试验. 在应用MVC框架时, Activity(或Fragment)代表View, 并从中剥离出不含任何的Android类(如Context等)的Controller. 相比较于原本项目, MVC架构具备:

使用Asset Catalogs来图像财富文件

从前的图像能源管理非常混乱,不止全部文件混杂在目录中,并且每一个能源对应着两到多少个例外分辨率的@2x, @3x古板位图。

行使Asset Catalog举办统一保管后,不仅仅工程干净恬适了不胜枚举,还足以用一张矢量图来让XCode自动为您转移差别分辨率的资源。记得自身刚才说的“面向以后”吗?xD

(没错,矢量图是自身本人拿Adobe Illustrator重绘的。)

ViewModel

每八个 View 都会有二个相应的 ViewModel,那个 ViewModel 会包罗展现这么些View 所急需的保有数据。
大家会使用工厂方法来创立 View Model,举个例子这些事例里,Subject View Model 无需关切传递给他是什么样的 Subject,全部的课目或然只是客商展开的科目。

@interface APEHomePracticeSubjectsViewModel : NSObject
@property (nonatomic, strong, nonnull) NSArray<APEHomePracticeSubjectsCollectionCellViewModel *>
*cellViewModels;
@property (nonatomic, strong, nonnull) UIColor *backgroundColor;
  (nonnull APEHomePracticeSubjectsViewModel *)viewModelWithSubjects:(nonnull NSArray<APESubject *>
 *)subjects;

@end

ViewModel 可以蕴含越来越小的 ViewModel,就疑似 View 能够有 SubView 同样。SubjectView 的中间是由二个UICollectionView兑现的,所以大家也给了对应的 Cell 设计了一个 ViewModel。
亟待额外注意的是,ViewModel 一般的话会富含的突显分界面所急需的保有因素,但粒度是足以操纵。一般的话,大家只把会因为事情转移而生成的局地设为 ViewModel 的一有的,比如这里的 titleColor 和 backgroundColor 会因为主题分歧而转变,但字体的大小(titleFont)却是不会变的,所以无需事无巨细的都加到 ViewModel 里。

@interface APEHomePracticeSubjectsCollectionCellViewModel : NSObject
@property (nonatomic, strong, nonnull) UIImage *image;
@property (nonatomic, strong, nonnull) UIImage *highlightedImage;
@property (nonatomic, strong, nonnull) NSString *title;
@property (nonatomic, strong, nonnull) UIColor *titleColor;
@property (nonatomic, strong, nonnull) UIColor *backgroundColor;
  (nonnull APEHomePracticeSubjectsCollectionCellViewModel *)viewModelWithSubject:(nonnull
APESubject *)subject;
  (nonnull APEHomePracticeSubjectsCollectionCellViewModel *)viewModelForMore;

@end
  1. 修改UI逻辑时, 很少修改Model;
  2. 修改工作逻辑时, 很少修改View.

设定合理的Logging战术

在此以前的品种中直接使用NSLog,那样的害处是在行业内部上线的App中很难把具有NSLog都屏蔽掉,而这一个输出的记录会影响程序功效,还或然败露隐秘。

一旦在一始发就设定合理的Logging分级编写制定,那样在打公布包的时候只需把Looging Level调高(乃至足以在切换scheme的时候自动举办调治),就会杜绝上述难点。

View

View 只要求定义好装配 ViewModel 的接口和概念好 UI 回调事件就可以:

@protocol APEHomePracticeSubjectsViewDelegate <NSObject>
- (void)homePracticeSubjectsView:(nonnull APEHomePracticeSubjectsView *)subjectView
             didPressItemAtIndex:(NSInteger)index;
@end
@interface APEHomePracticeSubjectsView : UIView
@property (nonatomic, strong, nullable, readonly) APEHomePracticeSubjectsViewModel *viewModel;
@property (nonatomic, weak, nullable) id<APEHomePracticeSubjectsViewDelegate> delegate;
- (void)bindDataWithViewModel:(nonnull APEHomePracticeSubjectsViewModel *)viewModel;
@end

渲染界面包车型地铁时候,完全依赖 ViewModel 举办,饱含 View 的 SubView 也会动用 ViewModel 里面包车型大巴子 ViewModel 渲染。

- (void)bindDataWithViewModel:(nonnull APEHomePracticeSubjectsViewModel *)viewModel {
    self.viewModel = viewModel;
    self.backgroundColor = viewModel.backgroundColor;
    [self.collectionView reloadData];
    [self setNeedsUpdateConstraints];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:
(NSIndexPath *)indexPath {
    APEHomePracticeSubjectsCollectionViewCell *cell = [collectionView
dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    if (0 <= indexPath.row && indexPath.row < self.viewModel.cellViewModels.count) {
        APEHomePracticeSubjectsCollectionCellViewModel *vm =
self.viewModel.cellViewModels[indexPath.row];
        [cell bindDataWithViewModel:vm];
}
    return cell;
}

迄今,我们就大功告成了有着的步调。大家回过头再看一下 ViewController 的天职就回变的特别轻易,装配好 View,向 DataController 需要数据,装配 ViewModel,配置给 View,接收 View 的UI事,一切繁杂的操作都能够的代办出去。

在被动情势中, View承继基类BaseView, Controller具有BaseView的援引, 更新数据; 在积极形式中, Model使用观察者通告View, 更新数据.

应用预管理表明

直白看图:

图片 21

行使Preprocessor Symbol的补益是何许吧?还以刚才的常量定义为例:

 struct CMConfig { ... #if Debug static let BaseURL : NSURL = NSURL(string: "https://test.itouchchina.com")! static let CMBPushMode : BPushMode = BPushMode.Development static let CMLogLevel : SLogLevel = SLogLevel.Verbose // 百度推送模式 static let CMDebugStatus : Bool = true static let CMHeaderClass: String = "test" #else static let BaseURL : NSURL = NSURL(string: "https://production.itouchchina.com")! static let CMBPushMode : BPushMode = BPushMode.Production static let CMLogLevel : SLogLevel = SLogLevel.Error static let CMDebugStatus : Bool = false static let CMHeaderClass: String = "production" #endif }

这样便能够一劳永逸地消除种种常量在开采版和正式版中的切换难点,你现在能够淡忘它,在切换scheme的时候全体都会自动调解,免去了恐怕的一无可取。

图片 22

若是您有越来越多的scheme,希望越来越细粒度地展开销配,能够选拔Swift Flag:

图片 23

在此不再赘述。

总结

MVC方式, 分离类的UI与作业任务, 扩展可测验性与可扩展性. Model不援引任何Android类, 允许单元测验(Unit Test). Controller含有View的援用, 不援用Android类, 允许单元测量检验. View知足纯净职责规范, 传递事件至Controller, 显示Model数据, 不含有业务逻辑, 允许UI测验.

运用CocoaPods实行第三方库管理

那一点应该我们都耳熟能详,哪个人用哪个人知道

上边选多少个有代表性的页面,分别证实一下立异的内部原因。

优点

因此地点的例子大家能够看看,这些架构有多少个优点:

档期的顺序明显,任务明确:和分界面有关的逻辑完全划到 ViewModel 和 View 一回,在那之中 ViewModel 负担分界面相关逻辑,View 负担绘制;Data Controller 肩负页面相关的数量逻辑,而 Model 依旧顶住纯粹的数据层逻辑。 ViewController 仅仅只是充当轻易的胶水成效。

耦合度低,测验性高:除开 ViewController 外,各类部件能够说是全然解耦合的,种种部分也是足以完全独立测量试验的。同三个作用,能够独家由不一样的开拓职员分别张开开拓分界面和逻辑,只需求建设构造好接口就可以。

复用性高:解耦合带来的额外受益正是复用性高,比方同二个View,只须要多三个厂子方法生成 ViewModel,就足以直接服用。数据逻辑代码不放在 ViewController 层也能够更有益于的服用。

上学花费低: 本质上的话,那么些框架结构属于对 MVC 的优化,重要在于减轻Massive View Controller 难题,把原先属于 View Controller 的任务依据分界面和逻辑部分相应的拆到 ViewModel 和 DataController 个中,所以是多个格外轻易明白的架构划设想计,纵然是菜鸟也足以急迅上手。

开辟开销低: 完全没有须求引进任何第三方库就足以开展支付,也制止了因为 MVVM 维护开销高的问题。

实践性高,重构开销低:能够在 MVC 架构上日益重构的架构,没有要求总身体重量写,是一种和 MVC 包容的安排。

View既正视于Controller又依附于Model. 在修改UI逻辑时, 也急需修改Model, 收缩架构的灵巧性. View与Model的职分部分重叠, 过于耦合, 在处理UI逻辑时, 被动格局与积极格局都会时有发生若干难点.

都会列表页

  1. 如前所述,UI层代码、交互层代码、响应层代码和数量处理以及开首化代码未有创设分离。

  2. 都市的下载状态不多个联结的职位持有。设计者的本意是经过步向该页面时生成的四个包含全数City实例的NSArray管理,可如果发轫下载,就要涉及DownloadManager模块里面包车型客车下载状态和近来页面数组中的状态之间的同台等一多级主题素材。同不时候,大家会发掘每一回在装置城市的下载状态时都会开展过多判别,这么些推断还应该有比相当多是双重的。更要紧的标题是,一个无冕的一无所长状态只怕会覆盖掉在此之前设置过的正确状态(因为在部分未有充分的上下文新闻对事态实行丰富的判别)。

    举个例证:

    if (cell.state == CGListTableCellStateEdit) { NSString * versionFile = getCGDocumentPath([NSString stringWithFormat: @"guides/guide%d/current.version", [guide.metroAppId intValue]]); NSString * version = [NSString stringWithContentsOfFile: versionFile encoding: NSUTF8StringEncoding error: nil]; NSFileManager * fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:versionFile]) { if ((version != nil) && ([guide.guideupdate compare: version] != NSOrderedSame)) cell.guide.cellState = CGListTableCellStateUpdate; else cell.guide.cellState = CGListTableCellStateDownloaded; }else{ cell.guide.cellState = CGListTableCellStateDownloaded; } [self reloadTableView];}
    

    那只是二个轻巧易行的当客商点击贰个编纂状态的都会时应有利用的动作,但此间的贯彻居然恐怖到使用了一个NSFileManager!而这整个仅仅是为了认清这几个都市应该复苏到已下载情景依然有更新状态。

    就像是的代码还是可以够观察一些处(在该页寻觅NSFileManager,有多达19个结果!)。

  3. 最终二个难点也是上边提到的,搭建UI选用相对坐标。以至有如此恶心的代码:

    - formatUI{ CGSize size = [cityNameLabel.text sizeWithFont:cityNameLabel.font]; CGRect rect; rect = cityNameLabel.frame; rect.size.width = size.width; cityNameLabel.frame = rect; if (size.width > 74) { // Why 74? CGRect rect; rect = sizeLabel.frame; rect.origin.x = 80   size.width - 74; // ??? sizeLabel.frame = rect; ... }}
    
#### 改进##### 下载管理在地铁通4.0中,我把所有对下载状态的管理一概收到了下载模块`CMDataAPI`中,现在获取下载状态只要一行:```swift var currentMode: CMCityListCellMode = CMDataAPI.sharedInstance.getModeForCity

getModeForCity(_city: CMCity)的贯彻中,只需遍历下载和平解决压五个种类,要是有须要对结果再实行时间戳的比对就可以。(顺便,作者把下载的时日戳迁移到了NSUserDefaults里,幸免了文本操作,并在运作时做了缓存。)

缺点

不可不可以认的是,那几个规划也可能有其对应的毛病,由于其把古板 MVVM 里面包车型客车 VM 拆成两局地,会照成下面包车型客车一些境况:

  1. 当页面包车型大巴互相逻辑非常多时,要求每每的在 DC-VC-VM 里来回传递音讯,造成了大气胶水代码。
  2. 除此以外,由于在守旧的 MVVM 中 VM 原来是环环相扣的,一些目眩神摇的互动本来能够在 VM 中中央银行政机关接到位测量试验,前段时间却须求同期采用 DC 和 VM 并附着一些胶水代码才具进行测量检验。
  3. 并未有了 Binding,代码写起来会更困难一点(个抒几见,各抒己见)。

被动情势中, Controller布告Model更新数据, 并文告View彰显. 对于UI逻辑, 若是View管理, 单元测验会遗漏逻辑; 借使Model管理, 则隐式地依附于View, 导致模块扩张耦合.

进程更新

而下载进程和解压进度的立异,也棉被服装进成了多个艺术,只暴光给调用页面须求的参数(以前的版本中,解压进度乃至是力不胜任查看的):

public func hookUpDownloadingProgress (_progress: ((UInt, Int64, Int64) -> Void)?, success: (CMCity -> Void)?, failure: (CMCity -> Void)?, forCity city: CMCity){ ...}public func hookUpUnzippingProgess (_progress: ( -> Void)?, success: (CMCity -> Void)?, forCity: city: CMCity){ ...}

后记

MVVM 是二个很棒的架构,私底下本身也会用其来做一些私家项目,但在店堂项目里,作者会更郑重的怀念之中利弊。小编做那个设计的时候,心仪 MVVM 的各种好处,又忌惮于它的各个缺陷,再思量到团体的付出和护卫花费,所以最后布署成了现行反革命这么。

村办认为,好的架构划设想计的都是和团伙以及业务场景皮之不存毛将焉附的。我们这套架构帮忙大家消除了 ViewController 代码聚积的主题材料,也带来了更清晰明了的代码层级和模块职分,同时未有引进过多的纷纭。希望大家也能丰盛知情那套架构的适用场景,在融洽的 APP 架构设计中装有借鉴。

// View处理UI逻辑String docName = userModel.getName();String docClinic = userModel.getClinic();nameTextView.setText(docName   ", "   docClinic)// Model处理UI逻辑String nameAndClinic = userModel.getNameAndClinic();nameTextView.setText(nameAndClinic);
与地图页的解耦合

在切换城市时,对地图页的直接调用改为发NSNotification

NSNotificationCenter.defaultCenter().postNotificationName(PropertyKeys.kCMCityChangeNotification, object: nil)

在意有所的播报名称常量也用前述的struct展开了合併的概念。

当仁不让形式中, 每一个UI逻辑都亟需充实阅览者, 保障科学更新.

改换数据操作

持有的数码早先化全体移到CMDataSource中进行:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in CMDataSource.sharedInstance.loadCity // One line to rule them all! dispatch_async(dispatch_get_main_queue(), {[unowned self] () -> Void in SVProgressHUD.dismiss() self.navigationController?.dismissViewControllerAnimated(true, completion: nil) })})

除去城市数据一致:

CMDataSource.sharedInstance.removeCity

MVC架构含有致命难点, 即View同偶尔候含有Controller与Model的引用; UI逻辑同期设有于View与Model之间. 那个标题导致事情逻辑与UI逻辑十分小概分开, 增添模块耦合, 影响重构. 那个主题素材在MVC的发展版MVP中逐年化解.

使用Storyboard/Xib

将UI搭建改为Storyboard/Xib那事早有过多钻探,略去不表。

文件 地铁通V3.x 地铁通V4.0
CityListViewController 1712行 280行
CityListCell 与上面是同一个文件 123行

That's all! Enjoy it!

路径结果页

  1. 如前所述,对于线路数量需求再开展加工技巧应用。

    如图,光是分明各样气象下的末班车时间就有下边一大堆方法:

    图片 24

    tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath里就越是悲戚。

  2. 其一页面在搭建UI上的难点更是严重。由于每一遍改换的不二等秘书诀有广大种可能的整合,比如该城市的数据库中有未有出租汽车车音讯、起点终点是客车站依然客户输入的地点、进行百度/谷歌寻觅时有寻觅二月搜索实现/退步三种情景……导致整个UI是在不停变化的,而未采纳AutoLayout中约束标准的坏处在这种场合下体现得彻底。

    恶意代码欣赏:

    if (taxiFee.floatValue < 0.01) { [headerView removeTaxi];}if(discountRate > 0){ [headerView setDiscountRateUI]; CGRect headerFrame = headerView.frame; headerFrame.size.height  = CM_ROUTE_SUM_MARGIN; headerView.frame = headerFrame;}
    

    已经设计员在挑UI难题的时候建议某处的空白留的相当不够,当时的自家企图了下在这种纷杂处境下想要正确调控空白高度的职业量……决定把那一个bug放到最终再改,后来另外要求一来就持续了之了……

  3. 直白操作现象。在数据变动要更新UI的时候,常常出现直接去立异相关View控件的代码。初看起来这没怎么难题,但要么刚刚说的主题素材,当那关乎到控件大小的更改时,就非凡令人胃疼了。

引进新数据类型

引进新的RouteRouteNode对象,将具备与表现层非亲非故的数目营造代码一律移入这里。

附带,还为今后大概的改观做了扩充:

convenience init(start:CMPOI, end:CMPOI, algorithm: CMRouteAlgorithm, type: CMRouteType)

在最早化对象的时候灵活传入所需的算法,能够兑现灵活调换差别的算法总计路径。以前出现过针对性国外城市采纳两样的路径总括格局,当时的贯彻弄得这几个恶心,这样就优雅相当多。

再顺便,这是策略模式——在面试中平时被问到“你在类型中用了怎么样设计方式”这种恶心的主题材料,专门查了眨眼之间间这种肤浅叫什么。

今后,就算是回来cell的回调方法里也清楚多了:

public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let node = routeArray[indexPath.row] if let type = node.type, let lineId = node.lineId{ switch type{ case .TimeAdjusting: var cell: CMRouteTimeAdjustingCell? = tableView.dequeueReusableCellWithIdentifier(TimeAdjustingCellId) as? CMRouteTimeAdjustingCell if cell == nil{ cell = CMRouteTimeAdjustingCell(style: UITableViewCellStyle.Default, reuseIdentifier: TimeAdjustingCellId) } ... // Initialization code return cell! case .Normal: var cell: CMRouteNormalStationCell? = tableView.dequeueReusableCellWithIdentifier(NormalStationCellId) as? CMRouteNormalStationCell ... // Same as above return cell! case .POI: ... return cell! case .Transfer, .Start, .Destination: ... return cell! } return UITableViewCell() }
采用约束原则创设UI

明天,整个页面,包含cell,都以用Constraints创设起来的(使用了多少个第三方库叫Snap基特)。

并且,cell里面包车型客车大巴线路,是用CoreGraphic投机画的。

依旧,线路上的文字是清水蓝依旧深褐,都以经过算法自动生成的。

图片 25

这么的灵活度基本上算是达到了:)

PS. 在完毕全部项目然后,作者试着把新版app在GALAXY Tab上跑了瞬间,惊叹地窥见,在自家从没改观任何地方的气象下,居然全体页面包车型客车UI都展现符合规律!那在引进约束原则生成UI之前简直是不可想像的。

只更新数据源,不操作控件

其实,那正是最宗旨的MVC分层在事实上中的应用——当数码变动时,产生变化的组件只肩负更新DataModel,然后简短地调用tableView.reloadData()解决。剩下的难点都由View层自行担当。

新兴在浏览博客时意识有关那样的转移,Eden有一篇《iOS 应用程式架构漫谈》大致是同等的景色,在那篇作品中表达得老大详尽,特别是:

小编们得以借鉴非常多”内部存款和储蓄器管理中的法则”,举个例子谁创建,谁销毁。同样,在大家的information flow中,大家期待谁创建Cache,谁更新Cache变化

这一句,道破本质。有意思味的可去观摩。

成果
文件 地铁通3.x 地铁通4.0
RouteViewController 3055行 1158行
RouteViewCell 158 67 171 89 220 249 245=1199行<1> 400行
Route<2> 1045行 N/A
DataModel<3> N/A 629行
DijkstraAlgorithm<4> N/A 320行

注:

  1. 3.x版本中分成NormalCell、TransferCell、HeaderView、SummaryView等相当多文件;
  2. 3.x版本中的Route文件其实是算法,生成二个不带有meta音讯的NSArray
  3. 4.0版本中的DataModel定义了程序所需的富有底层对象,不止有Route,还应该有下载的都市以及车站等等;
  4. 那是总结图上两点间最短路径的Dijkstra算法的贯彻,从以前的Route文件中抽了出去。

书签/历史页

以此页面从前也事关过,首要问题是计划性太拉杂。

修改之后的逻辑格外的凝练,想出错都难:

if indexPath.section == 0{//routes let route = favRoutes[indexPath.row] return getRouteCell(route as! NSDictionary)}else{//stations let station = favStations[indexPath.row] return getStationCell}

成果是:

文件 地铁通3.x 地铁通4.0
BookmarkViewController 784行 315行
BookmarkCell 109 98 105=312行 90行

车站实际情况页

以此页面包车型客车代码量与事先基本保持一致,分裂的是共同体的UI搭建逻辑。

事先的搭法:从上到下依次开头化子控件并累加,保持一个position变量记录当前中度。

新版中的搭法:各类子控件担负管理自身的UI展现,子控件之间用约束原则加以绑定。

鉴于这几个页面包车型客车数据皆以不明显的,因而不得不用代码增加数据成分间的牢笼原则,那是与前方那多少个大要UI已定好只待往里填数据的页面分裂的地点。

终极的画风差非常少是那样的:

scrollView.translatesAutoresizingMaskIntoConstraints = falsescrollView.addSubview(titleBackgroundView)scrollView.addSubview(timetableView)scrollView.addSubview(facilitiesView)scrollView.addSubviewself.view.addSubview(scrollView)scrollView.snp_makeConstraints {  -> Void in make.top.equalTo(self.view.snp_top) make.width.equalTo(self.view.snp_width) make.bottom.equalTo(floatingBGView.snp_top)}titleBackgroundView.snp_makeConstraints {  -> Void in make.top.equalTo(scrollView.snp_top).priorityRequired() make.width.equalTo(scrollView.snp_width).priorityRequired()}timetableView.snp_makeConstraints {  -> Void in make.top.equalTo(titleBackgroundView.snp_bottom).offset make.width.equalTo(scrollView.snp_width) make.centerX.equalTo(scrollView.snp_centerX)}facilitiesView.snp_makeConstraints {  -> Void in make.top.equalTo(timetableView.snp_bottom).offset make.width.equalTo(scrollView.snp_width) make.leading.equalTo(scrollView.snp_leading)}exitView.snp_makeConstraints {  -> Void in make.top.equalTo(facilitiesView.snp_bottom).offset make.leading.equalTo(scrollView.snp_leading) make.width.equalTo(scrollView.snp_width) make.bottom.equalTo(scrollView.snp_bottom).offset(-Constants.StationDetailViewVerticalSpacing)}if let _station = station{ titleLabel.text = _station.stationName iconsDisplayView.setImages(_station.getIcons timetableView.setup(_station.timeTables) facilitiesView.setupUI(_station.facilities) exitView.setupUI(_station.exits)}

UI约束与数码起始化势如破竹,并且清晰明了。(顺便感激Swift的链式语法)

搜索页

用到寻找页的地点有两处,贰个是首页点击顶端步入的页面,二个是首页点击下方右侧按键走入的页面。两个的不等是:前面一个有输入源点和终端的工具栏,前者只是一味找寻一定车站。

图片 26

  1. 事先的版本的七个分界面完全部独用立,等于好些个逻辑重复了三回,违背了DLANDY(Don't Repeat Yourself)原则。

  2. 和线路详细情形页类似,此处也缺乏对数码客观的对象化封装,导致在选取时索要多量的论断逻辑。比如,会现出上面那样的代码片段:

    if ([workDict objectForKey:@"city"] != nil && [[workDict objectForKey:@"city"] isEqualToString:_cityName]) { _isShowingWork = YES; if ([workDict objectForKey:@"add"] == nil) { // Station ... } else { // POI [self updateHistoryItem:workDict]; }}else{ // Have not set ...}
    

    这种直白对原本数据进行操作的做法其实是既麻烦又轻便出错。

  3. 对子控件的第一手更新,这一点和前面包车型地铁线路结果页类似,但更为劳苦:由于最近编写制定状态在每二三十一日变动,在支配创新控件时必须由零碎的变量来扶持推断究竟要对哪些控件举行操作。比方在百度一定API再次回到结果过后,上面包车型客车这段响应代码:

    - updatePOIFromLocation:(BMKPoiInfo *)info{Station *station = [[CMDataSource getInstance] getNearestStation:info.pt.latitude andLo:info.pt.longitude];if (isUpdatingStart) { ... // Do some work self.currentEditing = CMSearchEditingEnd;}else if (isUpdatingEnd){ ... // Doing work}if (isUpdatingStart) { isUpdatingStart = NO; _startIsShowingLocation = YES;}if (isUpdatingEnd) { isUpdatingEnd = NO; _endIsShowingLocation = YES;}
    

}

又要凭借状态变量进行判断,又要记得时刻更新它们,还得防止考虑不周更新错了状态,简直是车祸现场。#### 改进##### 合并两个页面我直接把两个页面合并到了一起,由入口参数决定最终显示哪个界面。但后来意识到,这并不是最好的解决方案。弊端是,原来是毫无耦合造成重复,现在两个页面的耦合又太紧了,虽然在这版的需求中工作得很好,再有调整又会比较棘手。更好的做法是使用`组合`,把公共的列表提取出来。这一点在Casa的[《跳出面向对象思想 继承》](http://casatwy.com/tiao-chu-mian-xiang-dui-xiang-si-xiang-yi-ji-cheng.html)中解释得非常好,我就不再赘述。##### 新定义数据对象在前面提到的`DataModel`中增加了`POI`对象,包含`Station`作为它的一个property。这代表,现在整个界面中,甚至整个app运行期间,都只有一种数据对象——`CMPOI`,清爽,干净,不紧绷。##### 只更新数据源,不操作控件基本同前,不赘述。##### 成果| 文件 | 地铁通3.x | 地铁通4.0||-----|:-------:|:------:||SearchStationViewController|2019行|469行||StationSearchView|821行|N/A|### DataSource#### 明确职责这个组件作为整个app的数据中心,在新版的开发中更加明确了它的职责,把原来分散在其他组件中的一些功能划了过来。比如,之前城市列表读取及解析功能是被划到网络请求组件中的,因为从服务器拿过来的返回数据可以就地解析;但从更高层的逻辑考虑这是不正确的,因为对程序城市列表的需求是更直接的,网络模块只要做好本职的网络交互就行了。修改之后,网络请求组件读取成功城市列表后,并不直接返回,而是将列表保存到本地缓存起来,再发一个Notification通知到关心读取成功这个事件的其他组件:```swiftfunc updateCityList(){ ... // Initialization code for paramaters let param: [String : String] = ["param": JSONStringParam] manager.POST(url, parameters: param, success: { (opreation, object) -> Void in let data = object as! NSData if CMDataSource.sharedInstance.loadXMLResponse == true{ let documentPath = getDocumentPath("guides/guides.info") data.writeToFile(documentPath, atomically: true) NSNotificationCenter.defaultCenter().postNotificationName(PropertyKeys.kCMCityListUpdateSuccessNotification, object: nil) } }) { (opreation, error) -> Void in ... // Error handler } }

将零件间的耦合降到最低。别的注意这段代码中对回到结果的剖析已经移到DataSource了。

正如下面提到的做法,此前好多对DataSource直白的操作,在这一版中有成千上万都改为了通过Notification交互:

init(){ let cityComponents = getCityListFromFile() ... // Data initialization NSNotificationCenter.defaultCenter().addObserver(self, #selector(CMDataSource.didSetStart, name: PropertyKeys.kCMSetStartStationNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, #selector(CMDataSource.didSetEnd, name: PropertyKeys.kCMSetEndStationNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, #selector(CMDataSource.didRemoveStart, name: PropertyKeys.kCMRemoveStartNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, #selector(CMDataSource.didRemoveEnd, name: PropertyKeys.kCMRemoveEndNotification, object: nil)} deinit{ NSNotificationCenter.defaultCenter().removeObserver}

其他

对此与地方提起的立异点相比较邻近的页面,限于篇幅不再赘述,只在这里一并提起:

  • 地图View,去掉了与上层的有着耦合,交互改为发NSNotification;同期将地图上要来得的控件统一到贰个protocol以下,那样就足以采纳斯维夫特新提供的泛型特点,写出很强一致性的代码:

    func updateCoor<T: TiledScrollViewOverlay where T: UIView>(_view: T, zoom: Bool){ ... // Update code}
    
  • 网络请求模块,上边已有聊起,在移除不属于本身逻辑的同一时候,重新整理了深入分析数据的空子(原本的本子中照旧重复着极为相似的三段代码!又三个车祸现场);

  • 分享模块,在此之前的卷入使用十三分繁琐,每一遍还要自行加到UIWindow上去。于是又再一次写了三个,弹出分享控件只需下边几步:

    let shareKit: CMShareKit = CMShareKit.getInstance()shareKit.setup("分享文字", emailBody: "邮件文字", smsBody: shareContent, weixinTitle: appName, weixinBody: shareContent, weixinPengyouBody: shareContent, weixinURL: CMConfig.AppDownloadURLTrim.absoluteString, qqTitle: appName, qqBody: shareContent, qqURL: CMConfig.AppDownloadURLTrim.absoluteString, weiboBody: shareContent, image: self.getCurrentViewShotshareKit.show
    

    新兴阅览马特 Gemmell在他的《API Design》那篇blog中也异口同声地关系了那一点:

    Rule 6: Get up and running in 3 linesExcluding delegate methods, you should aim to make it usable at least for testing purposes with only 3 lines of code.Those lines are:

    Instantiate it.Basically configure, so it will show and/or do something.Display or otherwise activate it.

    That should be it. Anything substantially more onerous is a code smell.

  • 添加/删除书签和历史模块,干掉,完全移入DataSource。並且前一本子是把多少类别化后保存为文件,那产生了一对一大的品质难点,在强力测验疯狂增加和删除时竟然也许引致崩溃。新版中改为只保留须要的新闻,等急需时再把路子实时总计出来即可。

地点基本把大块的重构页面讲完了,下边说多少个旧版本中平素不的页面,也好不轻巧作者要好的私心。

虚构到那是本身最后一次担当那一个项目,也是第一遍有那般大的话语权,小编禁不住在重构之余腾入手来加了有个别协和想加的页面。

谢谢页面

用作一个不怎么情怀的技师,笔者直接对于程序中绝非对开源库的蒙恩被德那或多或少刻骨铭心。从前对品种没有领导权,也腾不入手做,此番怎么也得补上:

图片 27

注:由于当下在揭橥时CocoaPods和斯威夫特还会有个别包容性难点,所以并未有用CocoaPods自动生成的,而是自个儿写了个HTML网页。

关于页

至于页也是夙愿之一。事实上更早的本子中有其一页面,但由于有些不可考的原故给砍掉了。

自己仿照在此以前的关于页用Photoshop作了个图,何况顺便把原先版本的有关页也加了进去,往左边滑动就能够知到。即便那样个深藏在n级页面之下的页面估摸没几个人点进去,但终归照旧为从前那七个早就离职的已经为大巴通出过力的同事在那么些角落立了个碑。

图片 28

历史版本

这一个规范作者是在盘活线路详细情形页的时候想到的:既然作者做了一套能够在cell上画线路的代码,那么自身得以把它复用到别的地方啊——举例整个大巴通的发展史!

于是就有了下面的页面:

图片 29

看起来很轻巧,其实做起来仍然略微费劲的。笔者在三朝跨年的时候去了趟东瀛休年假,途中带上了台式机计算机,那真是一一时光就在做那个页面啊……

图片 30

末班车提醒

那也是作者私心非常想做的功力,也借着此次机缘做了出来:

图片 31

上个动图演示:

图片 32

对于地铁线路来说,首班车很简短——只需从数据库中读取起源站的首班车音信就能够。可末班车却复杂得多,它既不是总结的源点站末班车时间,亦不是最终一个换乘站的末班车时间,而是有一点类似于木桶原理,取决于短板——相当于说,取决于整条换乘路线中收班最初的换乘站。

在前头的本子中,大家兑现了那一个算法,但平常有顾客表示困惑:那么些末班车是怎么算出来的?为何这么奇异?

地点的那些效果正是对这么些主题材料的雅致解答,一切都以自解释的:在客商操作的时候就能够只顾到变化,一句多余的验证都不用。

一经境遇标红字的站点的末班车,整条末班车线路就能够营造。

实则,从前的出品老董已经做出过一版规划:

图片 33

小编不希罕这几个企划,因为它割裂了上下文。弹窗是个特别影响相互体验的设计格局,除非您明显要显式地挑起客户注意,不然引进弹窗正是不对的。计算机出栈入栈尚且有开拓,何况人脑?(其他注意那些页面里的“首班车”和“末班车”按键——未有比这俩按键更突兀的宏图了)

后来那个功能在反对声中索性被砍掉了,但本人或许以为那一个需如若有须要的,只是需求找到更客观的安插性。

在找到以后以此技术方案在此以前也纠结了相当久,最后在有些早上能非常快完毕依然要归功于此番合理的架构划设想计,很难想象在事先那一坨两千行的代码里加个这种成效要抓狂到哪些地步。

彩蛋

没有错,作为一个有所不独有一点情感的程序猿……小编还埋了个彩蛋,猜想没人开掘过,纵然如此自己也不想说xD

PS. 这三个大公司的软件里彩蛋到底是怎么埋的?一Code Review不是就直接被辞退了呢?

实际,小编还抽空稍微规划了一晃surface版本的大巴通:

图片 34

但鉴于时日实际上是相当不够用,最后作罢。

核心写完了程序主体,然后笔者又起来做起了市镇。

当即赶工作时间每一天独一的玩乐是伴饭看一集《广告狂人》,所以到这几个想广告词的环节感到非常带感,认为Don Draper(《广告狂人》男一号)上身。

于是就有了本人用Photoshop做的下面这一堆AppStore宣传图:

图片 35

扶上驴,送一程。至此,对于这一个项目,笔者的任务大约完结了。

客商反馈大致是如此:

图片 36

就用项目刚开始时笔者在豆瓣发的一条广播来了却本文吧:

图片 37

希望能给你带来一些启发,迎接建议意见和钻探。

感激观赏。

广告:

自身今日正值找事业,希望插足一个非常大的团伙延续读书。

如果您以为本人比较适当,能够联系:zshowing[at]gmail.com

本文由星彩网app下载发布于计算机编程,转载请注明出处:0版iOS客户端重写中的一些经验,完全解析Androi

TAG标签: 星彩网app下载
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。