本文做为这一系列的收尾总结, 详细叙述了这个架构工具的设计思路以及一步步的优化, 在此也分享与你, 完整
keynote
可查阅githubhtml
本文做为以上文章系列的总结, 如何一步一步进行思考总结, 如何开发出适合本身的通用架构设计.前端
对于架构, 移动端常见的架构设计包括MVC
, MVVM
, MVP
等, 上图简要的说明了各类常见的架构之间的交互及数据传递方式.java
对于MVC
, MVVM
, MVP
这三种架构设计模式, 相信你们必定了然于心, 相关的文章也是多如繁星, 对于这些经常使用架构, 每一个人都确定有每一个人的理解, 但这样会致使一个问题, 就是极大的自由度致使了没有代码规范, 对于移动端或者前端及后端来讲, 其本质工做就是数据层和展现层的交互, 如何将数据正确安全高效的传输到展现层.react
这里的数据层从整个项目来讲, 能够说是后端, 也就是服务端, 对于服务端开发的流程就是从数据库获取数据并将数据进行各类逻辑过滤做为响应返回给前端用于展现层展现, 对于java为例, 咱们普通的项目就会分为controller
, service
, dao
, pojo
, vo
, bo
等层级设计, 就会将不一样功能进行抽象, 使得代码更容易维护.webpack
而做为展现层的前端, 也就是客户端, 其实移动端在我感受其实也是前端的一个分支, 而前端的架构一般为组件化设计, 每个功能view
对应一个组件, 而整个页面能够经过多个组件分离进行维护, 很高效的将业务代码和视图进行分离, 使得代码更有规范及易维护.git
而对于移动端, 为何要在controller
中写那么多不知所云的代码? 为何一个控制器能超过1k行? 为何view
的逻辑回调代理要写在controller
中? 为何控制器之间的参数传递的耦合性那么强? 为何网络请求的方法随处可见? 为何咱们不可以像先后端那样有条理的控制咱们的代码? 而让其像脱缰的野马难以驾驭呢?github
为了解决这些问题, 咱们须要考虑一些架构设计模式, 最早想到的就是以controller
为中心的抽象, 将控制器的功能抽象到该具体负责的模块, 从图上能够看到, controller
维护了presenter
, viewmodel
, view
, 而viewmodel
又维护了model
, 其中的model
也能够说就是javabean
彻底的纯数据结构, viewmodel
是model
的上一层, 用于操做对应的数据, 能够看到controller
将代码下发至下面三层, 使得各个层级各司其职.web
刚才是站在controller
的视角上来看的, 对于数据的传输, 此次咱们站在presenter
的视角上来看, 这个设计就是将viewmodel
做为传递对象经过presenter
这个中间件传输至view
层, 这样view层不只能够拿到数据, 也能够对数据进行操做, 掌控性有所提升.数据库
刚才咱们站在了controller
和presenter
视角上分析了架构设计的思路, 但这样各个层级的耦合会愈来愈大, 从而致使项目代码没法分割, 这时想到了后端controller
和service
之间经过接口进行交互来下降耦合, 咱们是否是也能够参考这种方案经过一个protocol
文档文件来下降各个层级之间的耦合呢, 如图所示, 将除了controller
以外的其余层级进行解耦. 进行高度抽象.npm
上面咱们解决了各个层级之间的耦合, 但咱们怎么解决控制器之间的耦合呢? 答案是router
, 咱们站在路由的视角上看, 各大控制器都是独立存在的个体, 彼此之间的交互经过路由的映射进行交互, 这样咱们就可以去除各大控制器之间的耦合了. 路由这个思路最早也是在前端的架构框架中看到的, 后来就有了cocoapods
私有库组件化这种集成化的解决方案, 经过路由映射可以很好的作到模块分离, 更能够作到页面降级, 所谓的页面降级就是指不只路由能够和native
进行交互, 也能够和h5
进行交互, 当native
和h5
是一套业务逻辑的时候, native
不慎出现bug
咱们能够请求后端接口修改数据库将页面直接降级至h5
页面而不用从新打包等待苹果审核及使用热修复工具带来了时间消耗. 可以第一时间解决问题.
解决了上述问题, 咱们就只剩下页面的问题了, 对于如今的iOSer来讲, 写页面几乎是平常工做的绝大部分, 可是写页面, 写业务, 当逻辑复杂的时候也会产生一系列不易维护的问题, 这时候咱们就可使用相似redux
这种状态机的模式, 将业务逻辑拆分出不一样复杂的状态, 当变量改变的时候, 触发不一样的状态, 这样就可以有效的管理咱们的页面逻辑. 推荐能够看看react
和redux
的思路, 对这块也会有更好的掌握.
设计思路的总结就是, 经过高度抽象进行分层, 经过接口文档进行项目层级解耦, 经过路由进行组件化及降级, 经过CDD
模式贯穿subview
使得全部的view
都可以拿到数据及操控数据, 经过AOP
切面进行hook
一些特殊功能如埋点统计, 全局当前控制器等等.
上面部分, 叙述了整个架构的设计思路, 接下来, 咱们来看看如何具体实现. 如下代码取自真实项目, 对应view视角
图中的设计图.
//
// InterfaceTemplate.h
// SQTemplate
//
// Created by 双泉 朱 on 17/5/5.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol HYAbroadShoppingHomeModelInterface <NSObject>
/**
* 仅用来保持PB不为空
*/
@property(nonatomic) NSInteger status ;
/**
* 广告位
*/
@property(nonatomic,strong) NSMutableArray * banners ;
/**
* 四个icon
*/
@property(nonatomic,strong) NSMutableArray * shortCutIcons ;
/**
* 促销时间戳,活动剩余时间,转换成毫秒
*/
@property(nonatomic,strong) NSString * remainingTime ;
/**
* 倒计时的商品,客户端根据当该字段有的时候,展现“今日剁手价”图片
*/
@property(nonatomic,strong) NSMutableArray * salesGoods ;
/**
* 全球精选商品集合
*/
@property(nonatomic,strong) NSMutableArray * selectGoods ;
@property (nonatomic,assign,getter=isLoaded) BOOL loaded;
@property (nonatomic,assign,getter=isReload) BOOL reload;
@end
@protocol HYAbroadShoppingHomeViewModelInterface <NSObject>
@optional
@property (nonatomic,strong) id<HYAbroadShoppingHomeModelInterface> model;
@optional
- (void)initializeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion;
/**
*当即购买
* @para goodsId 这里Android就去调用CommonUtils里面的方法便可。IOS这里自行添加相应的代码。注意这里保持一个逻辑:若是是处方药进入到商品详情页,隐形眼镜…..
*/
- (void)senderAddShoppingCartWithModel:(id<HYAbroadShoppingHomeModelInterface>)model goodsId:(GoodsID *)goodsId completion:(void(^)())completion;
/**
*获取海外购首页
*/
- (void)senderAbroadShoppingHomeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion;
@end
@protocol HYAbroadShoppingHomeViewInterface <NSObject>
@property (nonatomic,strong) id<HYAbroadShoppingHomeViewModelInterface> abroadshoppinghomeViewModel;
@property (nonatomic,strong) id<HYAbroadShoppingHomeViewModelInterface> abroadshoppinghomeOperator;
@end
复制代码
接口文档文件将整个页面模块分红了ModelInterface
, ViewModelInterface
, ViewInterface
三个接口,ModelInterface
接口对应了服务器返回的外层数据结构, ViewModelInterface
接口对应了操做model
数据的方法, 如发起请求和从数据库读取诸如此类. ViewInterface
拥有二者的能力.
//
// ControllerTemplate.m
// SQTemplate
//
// Created by 双泉 朱 on 17/5/5.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
#import "HYAbroadShoppingHomeViewController.h"
#import "HYAbroadShoppingHomePresenter.h"
#import "HYAbroadShoppingHomeViewModel.h"
#import "HYAbroadShoppingHomeView.h"
@interface HYAbroadShoppingHomeViewController ()
@property (nonatomic,strong) HYAbroadShoppingHomePresenter * abroadshoppinghomePresenter;
@property (nonatomic,strong) HYAbroadShoppingHomeViewModel * abroadshoppinghomeViewModel;
@property (nonatomic,strong) HYAbroadShoppingHomeView * abroadshoppinghomeView;
@end
@implementation HYAbroadShoppingHomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"全球购";
[self setupView];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self adapterView];
}
- (HYAbroadShoppingHomePresenter *)abroadshoppinghomePresenter {
if (!_abroadshoppinghomePresenter) {
_abroadshoppinghomePresenter = [HYAbroadShoppingHomePresenter new];
}
return _abroadshoppinghomePresenter;
}
- (HYAbroadShoppingHomeViewModel *)abroadshoppinghomeViewModel {
if (!_abroadshoppinghomeViewModel) {
_abroadshoppinghomeViewModel = [HYAbroadShoppingHomeViewModel new];
}
return _abroadshoppinghomeViewModel;
}
- (HYAbroadShoppingHomeView *)abroadshoppinghomeView {
if (!_abroadshoppinghomeView) {
_abroadshoppinghomeView = [HYAbroadShoppingHomeView new];
_abroadshoppinghomeView.frame = self.view.bounds;
}
return _abroadshoppinghomeView;
}
- (void)setupView {
[self.view addSubview:self.abroadshoppinghomeView];
}
- (void)adapterView {
[self.abroadshoppinghomePresenter adapterWithAbroadShoppingHomeView:self.abroadshoppinghomeView abroadshoppinghomeViewModel:self.abroadshoppinghomeViewModel];
}
@end
复制代码
通过抽象后, 咱们再来看看controller
的文件, 咱们能够看到, 通过抽象后的控制器加上顶部的注释也只有68
行, 基本是不用再用心维护上千行的控制器了.
//
// PresenterTemplate.m
// SQTemplate
//
// Created by 双泉 朱 on 17/5/5.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
#import "HYAbroadShoppingHomePresenter.h"
@interface HYAbroadShoppingHomePresenter ()
@property (nonatomic,weak) id<HYAbroadShoppingHomeViewInterface> abroadshoppinghomeView;
@property (nonatomic,weak) id<HYAbroadShoppingHomeViewModelInterface> abroadshoppinghomeViewModel;
@end
@implementation HYAbroadShoppingHomePresenter
- (void)adapterWithAbroadShoppingHomeView:(id<HYAbroadShoppingHomeViewInterface>)abroadshoppinghomeView abroadshoppinghomeViewModel:(id<HYAbroadShoppingHomeViewModelInterface>)abroadshoppinghomeViewModel {
_abroadshoppinghomeView = abroadshoppinghomeView;
_abroadshoppinghomeViewModel = abroadshoppinghomeViewModel;
__weak typeof(self) _self = self;
__weak id<HYAbroadShoppingHomeViewModelInterface> __abroadshoppinghomeViewModel = _abroadshoppinghomeViewModel;
[_abroadshoppinghomeViewModel initializeWithModel:__abroadshoppinghomeViewModel.model completion:^{
_self.abroadshoppinghomeView.abroadshoppinghomeViewModel = __abroadshoppinghomeViewModel;
_self.abroadshoppinghomeView.abroadshoppinghomeOperator = _self;
}];
}
/**
*当即购买
* @para goodsId 这里Android就去调用CommonUtils里面的方法便可。IOS这里自行添加相应的代码。注意这里保持一个逻辑:若是是处方药进入到商品详情页,隐形眼镜…..
*/
- (void)senderAddShoppingCartWithModel:(id<HYAbroadShoppingHomeModelInterface>)model goodsId:(GoodsID *)goodsId completion:(void(^)())completion {
__weak typeof(self) _self = self;
__weak id<HYAbroadShoppingHomeViewModelInterface> __abroadshoppinghomeViewModel = _abroadshoppinghomeViewModel;
[_abroadshoppinghomeViewModel senderAddShoppingCartWithModel:model goodsId:goodsId completion:^{
_self.abroadshoppinghomeView.abroadshoppinghomeViewModel = __abroadshoppinghomeViewModel;
completion();
}];
}
/**
*获取海外购首页
*/
- (void)senderAbroadShoppingHomeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion {
__weak typeof(self) _self = self;
__weak id<HYAbroadShoppingHomeViewModelInterface> __abroadshoppinghomeViewModel = _abroadshoppinghomeViewModel;
[_abroadshoppinghomeViewModel senderAbroadShoppingHomeWithModel:model completion:^{
_self.abroadshoppinghomeView.abroadshoppinghomeViewModel = __abroadshoppinghomeViewModel;
completion();
}];
}
@end
复制代码
在controller
中, 咱们将viewmodel
和view
, 也就是上述的数据层和展现层传输到 presenter
的中间件进行交互, 咱们经过观察能够看到当请求完成后, 先进行赋值操做, 再进行自定义业务逻辑, 这样可以保证操做业务逻辑时数据是最新的.
//
// ViewModelTemplate.m
// SQTemplate
//
// Created by 双泉 朱 on 17/5/5.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
#import "HYAbroadShoppingHomeViewModel.h"
#import "HYAbroadShoppingHomeModel.h"
#import "HYAbroadShoppingSender.h"
#import "HYMallGoodsSender.h"
@implementation HYAbroadShoppingHomeViewModel
- (HYAbroadShoppingHomeModel *)model {
if (!_model) {
_model = [HYAbroadShoppingHomeModel new];
}
return _model;
}
- (void)initializeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion {
if (!model.isLoaded) {
[self senderAbroadShoppingHomeWithModel:model completion:completion];
}
}
/**
*当即购买
* @para goodsId 这里Android就去调用CommonUtils里面的方法便可。IOS这里自行添加相应的代码。注意这里保持一个逻辑:若是是处方药进入到商品详情页,隐形眼镜…..
*/
- (void)senderAddShoppingCartWithModel:(id<HYAbroadShoppingHomeModelInterface>)model goodsId:(GoodsID *)goodsId completion:(void(^)())completion {
NSMutableArray * editArray=[@[] mutableCopy];
EditGoodsM_Builder * editGoodsM_Builder=[[EditGoodsM_Builder alloc]init];
editGoodsM_Builder.goodsId = goodsId;
editGoodsM_Builder.amount = 1;
[editArray addObject:[editGoodsM_Builder build]];
HYSenderResultModel * resultModel = [HYMallGoodsSender senderAddShoppingCart:nil token:nil editGoods:editArray promoteIDs:nil];
HYViewController * vc = [HYCurrentVCmanager shareInstance].getCurrentVC;
[vc startLoading];
[vc requestWithModel:resultModel success:^(HYResponseModel *model) {
_model.reload = NO;
completion();
[vc endLoading];
} failure:^(HYResponseModel * model) {
[vc endLoading];
}];
}
/**
*获取海外购首页
*/
- (void)senderAbroadShoppingHomeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion {
HYSenderResultModel * resultModel = [HYAbroadShoppingSender senderAbroadShoppingHome:model status:0];
HYViewController * vc = [HYCurrentVCmanager shareInstance].getCurrentVC;
[vc startLoading];
[vc requestWithModel:resultModel success:^(HYResponseModel *model) {
_model.loaded = YES;
completion();
[vc endLoading];
} failure:^(HYResponseModel * model) {
[vc endLoading];
}];
}
@end
复制代码
咱们再来看看viewmodel
层如何设计, viewmodel
层持有model
, 并进行数据获取, 将获取的数据赋值到model
中, 因为线上真实项目使用的是TCP
+ProtoBuffer
, 代码显示的是我司自行封装的一套网络逻辑, 因此可能对一些同窗不是很友好, 请看下面的例子:
//
// ViewModelTemplate.m
// SQTemplate
//
// Created by 双泉 朱 on 17/5/5.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
#import "ViewModelTemplate.h"
#import "ModelTemplate.h"
#import "NetWork.h"
#import "DataBase.h"
@implementation ViewModelTemplate
- (void)dynamicBindingWithFinishedCallBack:(void (^)())finishCallBack {
[DataBase requestDataWithClass:[ModelTemplate class] finishedCallBack:^(NSDictionary *response) {
_model = [ModelTemplate modelWithDictionary:response];
finishCallBack();
}];
[NetWork requestDataWithType:MethodGetType URLString:@"http://localhost:3001/api/J1/getJ1List" parameter:nil finishedCallBack:^(NSDictionary * response){
_model = [ModelTemplate modelWithDictionary:response[@"data"]];
[DataBase cache:[ModelTemplate class] data:response[@"data"]];
finishCallBack();
}];
}
@end
复制代码
其中DataBase
仅仅是plist
的缓存, 而NetWork
是封装的AFNetworking
, 这样大部分同窗就很熟悉了吧, 在viewmodel
层能够将数数据库和网络请求这两种获取数据的方式封装在一个层级里面, 这样逻辑分明也对外界没有耦合, 而对于咱们线上项目咱们在底层还有一个sender
层用于管理上述问题及一些其余业务逻辑, 通用的架构设计是一个思想, 须要结合实际业务逻辑进行调整, 正所谓不能脱离业务谈架构.
//
// ViewTemplate.m
// SQTemplate
//
// Created by 双泉 朱 on 17/5/5.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
#import "HYAbroadShoppingHomeView.h"
#import "HYAbroadShoppingHomeHeaderView.h"
#import "HYAbroadSelectGoodsViewCell.h"
@interface HYAbroadShoppingHomeView () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic,strong) UITableView * tableView;
@property (nonatomic,strong) HYAbroadShoppingHomeHeaderView * headerView;
@end
@implementation HYAbroadShoppingHomeView
- (void)dealloc {
NSLog(@"%@ - execute %s",NSStringFromClass([self class]),__func__);
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupSubviews];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self setupSubviews];
}
return self;
}
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [UITableView new];
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.tableHeaderView = self.headerView;
_tableView.backgroundColor = SQBGC;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
return _tableView;
}
- (HYAbroadShoppingHomeHeaderView *)headerView {
if (!_headerView) {
_headerView = [HYAbroadShoppingHomeHeaderView new];
_headerView.hidden = YES;
}
return _headerView;
}
- (void)setupSubviews {
[self addSubview:self.tableView];
}
- (void)setAbroadshoppinghomeViewModel:(id<HYAbroadShoppingHomeViewModelInterface>)abroadshoppinghomeViewModel {
_abroadshoppinghomeViewModel = abroadshoppinghomeViewModel;
if (abroadshoppinghomeViewModel.model.reload) {
_headerView.hidden = !abroadshoppinghomeViewModel.model.isLoaded;
_headerView.model = abroadshoppinghomeViewModel.model;
CGFloat headerViewH = abroadshoppinghomeViewModel.model.remainingTime.length
? kscaleDeviceLength(160) + (self.width / 4) * 1.1 + 290
: kscaleDeviceLength(160) + (self.width / 4) * 1.1 + 50;
_headerView.frame = CGRectMake(0, 0, 0, headerViewH);
[_tableView reloadData];
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _abroadshoppinghomeViewModel.model.selectGoods.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
HYAbroadSelectGoodsViewCell * cell = [HYAbroadSelectGoodsViewCell cellWithTableView:tableView];
cell.good = _abroadshoppinghomeViewModel.model.selectGoods[indexPath.item];
cell.abroadshoppinghomeOperator = _abroadshoppinghomeOperator;
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [HYAbroadSelectGoodsViewCell cellHeightWithGood:_abroadshoppinghomeViewModel.model.selectGoods[indexPath.item]];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (void)layoutSubviews {
[super layoutSubviews];
_tableView.frame = self.bounds;
}
@end
复制代码
接着咱们来看view
,根据以前的设计图, 咱们将其区分为两个模块, tableview
的headerview
和tableviewcell
, 其中headerview
负责上面不须要复用的view
, 而tableviewcell
负责须要复用的部分.
这里咱们看到重写set
方法时根据是否须要刷新tableview
, 必定限度避免了无效的刷新消耗.
还有一个知识点是将operator
直接贯穿传递到各级subview
这里用到的就是CDD
模式的精髓, 使得全部的subview
都可以获取数据, 避免了delegate
, block
这种冗余回调带来耦合的尴尬, 关键是丑.
//
// HYAbroadSelectGoodsViewFrameHub.m
// Mall
//
// Created by 朱双泉 on 19/10/2017.
// Copyright © 2017 _Zhizi_. All rights reserved.
//
#import "HYAbroadSelectGoodsViewFrameHub.h"
#import "NSString+SQExtension.h"
@implementation HYAbroadSelectGoodsViewFrameHub
- (instancetype)initWithGoodsName:(NSString *)goodsName nameGoodEvalution:(NSString *)nameGoodEvalution {
self = [super init];
if (self) {
CGFloat width = DeviceWidth - 30;
CGFloat proImageUrlButtonX = 10;
CGFloat proImageUrlButtonY = proImageUrlButtonX;
CGFloat proImageUrlButtonW = width - 2 * proImageUrlButtonX;
CGFloat proImageUrlButtonH = proImageUrlButtonW;
_proImageUrlButtonFrame = CGRectMake(proImageUrlButtonX, proImageUrlButtonY, proImageUrlButtonW, proImageUrlButtonH);
CGFloat goodsSellerImageViewX = proImageUrlButtonX;
CGFloat goodsSellerImageViewY = proImageUrlButtonY + proImageUrlButtonH + 13;
CGFloat goodsSellerImageViewW = 20;
CGFloat goodsSellerImageViewH = 15;
_goodsSellerImageViewFrame = CGRectMake(goodsSellerImageViewX, goodsSellerImageViewY, goodsSellerImageViewW, goodsSellerImageViewH);
CGFloat goodsNameLabelX = goodsSellerImageViewX;
CGFloat goodsNameLabelY = proImageUrlButtonY + proImageUrlButtonH + 10;
CGFloat goodsNameLabelW = proImageUrlButtonW;
CGSize goodsNameLabelSize = [goodsName getSizeWithConstraint:CGSizeMake(goodsNameLabelW, 60) font:KF03_17];
CGFloat goodsNameLabelH = goodsNameLabelSize.height;
_goodsNameLabelFrame = CGRectMake(goodsNameLabelX, goodsNameLabelY, goodsNameLabelW, goodsNameLabelH);
CGFloat costPriceLabelY = 0.0;
if (nameGoodEvalution.length) {
CGFloat userEvalutionLabelX = proImageUrlButtonX + 10;
CGFloat userEvalutionLabelY = goodsNameLabelY + goodsNameLabelH + 10;
CGFloat userEvalutionLabelW = 55;
CGFloat userEvalutionLabelH = 20;
_userEvalutionLabelFrame = CGRectMake(userEvalutionLabelX, userEvalutionLabelY, userEvalutionLabelW, userEvalutionLabelH);
CGFloat userEvalutionBackgroundX = proImageUrlButtonX;
CGFloat userEvalutionBackgroundY = userEvalutionLabelY + userEvalutionLabelH / 2;
CGFloat userEvalutionBackgroundW = proImageUrlButtonW;
CGFloat nameGoodEvalutionLabelX = userEvalutionBackgroundX + 10;
CGFloat nameGoodEvalutionLabelY = userEvalutionLabelY + userEvalutionLabelH + 10;
CGFloat nameGoodEvalutionLabelW = userEvalutionBackgroundW - 20;
CGSize nameGoodEvalutionLabelSize = [nameGoodEvalution getSizeWithConstraint:CGSizeMake(nameGoodEvalutionLabelW, 40) font:KF06_12];
CGFloat nameGoodEvalutionLabelH = nameGoodEvalutionLabelSize.height;
_nameGoodEvalutionLabelFrame = CGRectMake(nameGoodEvalutionLabelX, nameGoodEvalutionLabelY, nameGoodEvalutionLabelW, nameGoodEvalutionLabelH);
CGFloat userEvalutionBackgroundH = nameGoodEvalutionLabelH + 30;
_userEvalutionBackgroundFrame = CGRectMake(userEvalutionBackgroundX, userEvalutionBackgroundY, userEvalutionBackgroundW, userEvalutionBackgroundH);
costPriceLabelY = userEvalutionBackgroundY + userEvalutionBackgroundH + 10;
} else {
costPriceLabelY = goodsNameLabelY + goodsNameLabelH + 10;
}
CGFloat costPriceLabelX = goodsNameLabelX;
CGFloat costPriceLabelW = 180;
CGFloat costPriceLabelH = 30;
_costPriceLabelFrame = CGRectMake(costPriceLabelX, costPriceLabelY, costPriceLabelW, costPriceLabelH);
CGFloat buyButtonW = 80;
CGFloat buyButtonX = goodsNameLabelX + goodsNameLabelW - buyButtonW;
CGFloat buyButtonY = costPriceLabelY;
CGFloat buyButtonH = costPriceLabelH;
_buyButtonFrame = CGRectMake(buyButtonX, buyButtonY, buyButtonW, buyButtonH);
_calculateHeight = CGRectGetMaxY(_buyButtonFrame) + 10;
}
return self;
}
@end
复制代码
当须要动态计算高度的时候, 咱们可使用framehub
这种模式, 名字是本身取的, 请别见怪, 对性能有要求的同窗能够将高度计算值缓存下来, 以避免cpu
重复大量计算致使手机的耗电.
//
// HYAbroadSelectGoodsViewCell.m
// Mall
//
// Created by 朱双泉 on 12/10/2017.
// Copyright © 2017 _Zhizi_. All rights reserved.
//
#import "HYAbroadSelectGoodsViewCell.h"
#import "HYAbroadSelectGoodsView.h"
#import "HYGoodsDetailViewController.h"
#import "HYCartNewViewController.h"
#import "UIAlertView+SQExtension.h"
#import "NSString+SQExtension.h"
@interface HYAbroadSelectGoodsViewCell ()
@property (nonatomic,strong) HYAbroadSelectGoodsView * selectGoodsView;
@end
@implementation HYAbroadSelectGoodsViewCell
- (void)dealloc {
#if DEBUG
NSLog(@"--------");
NSLog(@"%@ - execute %s",NSStringFromClass([self class]),__func__);
NSLog(@"--------");
#endif
}
+ (instancetype)cellWithTableView:(UITableView *)tableView {
NSString * identifier = NSStringFromClass([HYAbroadSelectGoodsViewCell class]);
HYAbroadSelectGoodsViewCell * cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[HYAbroadSelectGoodsViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
return cell;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self setupSubviews];
}
return self;
}
- (HYAbroadSelectGoodsView *)selectGoodsView {
if (!_selectGoodsView) {
_selectGoodsView = [HYAbroadSelectGoodsView new];
_selectGoodsView.backgroundColor = [UIColor whiteColor];
_selectGoodsView.layer.cornerRadius = 4;
_selectGoodsView.layer.masksToBounds = YES;
}
return _selectGoodsView;
}
- (void)setupSubviews {
self.contentView.backgroundColor = SQBGC;
[self.contentView addSubview:self.selectGoodsView];
}
- (void)setGood:(AbroadGoods *)good {
_good = good;
__weak typeof(self) _self = self;
[_selectGoodsView.proImageUrlButton sd_setBackgroundImageWithURL:[NSURL URLWithString:good.proImageUrl] forState:0 placeholderImage:[UIImage imageNamed:@"placeholder_200"]];
[_selectGoodsView.proImageUrlButton whenTapped:^{
[[HYCurrentVCmanager shareInstance].getCurrentVC hyPushDetail:_self.good.targetUrl];
}];
[_selectGoodsView.goodsSellerImageView sd_setImageWithURL:[NSURL URLWithString:good.goodsSellerImage]];
_selectGoodsView.goodsNameLabel.text = [NSString stringWithFormat:@" %@", [good.goodsName trim]];
_selectGoodsView.nameGoodEvalutionLabel.text = [good.nameGoodEvalution trim];
_selectGoodsView.costPriceLabel.text = good.ecPrice;
[_selectGoodsView.buyButton whenTapped:^{
if (_self.good.goodsType == GoodsTypePrescriptionAllow ||
_self.good.goodsType == GoodsTypePrescriptionForbid ||
_self.good.goodsType == GoodsTypeGlasses) {
[[HYCurrentVCmanager shareInstance].getCurrentVC HYPushViewController:[HYGoodsDetailViewController new] animated:YES];
} else {
[_self.abroadshoppinghomeOperator senderAddShoppingCartWithModel:nil goodsId:_self.good.goodsId completion:^{
[UIAlertView showAlertViewWithTitle:@"添加成功!" message:@"商品已加入购物车" cancelButtonTitle:@"再逛逛" otherButtonTitles:@[@"去购物车"] clickAtIndex:^(NSInteger buttonIndex) {
if (buttonIndex == 1) {
[[HYCurrentVCmanager shareInstance].getCurrentVC HYPushViewController:[HYCartNewViewController new] animated:YES];
}
}];
}];
}
}];
[_selectGoodsView setNeedsLayout];
[self setNeedsLayout];
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat selectGoodsViewX = 15;
CGFloat selectGoodsViewY = 0;
CGFloat selectGoodsViewW = self.width - 2 * selectGoodsViewX;
CGFloat selectGoodsViewH = [HYAbroadSelectGoodsView viewHeightWithGoodsName:[NSString stringWithFormat:@" %@", [_good.goodsName trim]] nameGoodEvalution:[_good.nameGoodEvalution trim]];
_selectGoodsView.frame = CGRectMake(selectGoodsViewX, selectGoodsViewY, selectGoodsViewW, selectGoodsViewH);
}
+ (CGFloat)cellHeightWithGood:(AbroadGoods *)good {
return [HYAbroadSelectGoodsView viewHeightWithGoodsName:[NSString stringWithFormat:@" %@", [good.goodsName trim]] nameGoodEvalution:[good.nameGoodEvalution trim]] + 10;
}
@end
复制代码
能够看到, 推荐将tableviewcell
的高度及获取封装在内, 避免和tableview
进行耦合, 这里注意的是如下代码:
[_self.abroadshoppinghomeOperator senderAddShoppingCartWithModel:nil goodsId:_self.good.goodsId completion:^{
[UIAlertView showAlertViewWithTitle:@"添加成功!" message:@"商品已加入购物车" cancelButtonTitle:@"再逛逛" otherButtonTitles:@[@"去购物车"] clickAtIndex:^(NSInteger buttonIndex) {
if (buttonIndex == 1) {
[[HYCurrentVCmanager shareInstance].getCurrentVC HYPushViewController:[HYCartNewViewController new] animated:YES];
}
}];
}];
复制代码
直接在subview
中获取了请求的逻辑, 当调用opeator
的方法时会经过presenter
中间件传递给viewmodel
进行请求, 当请求成功后进行赋值操做刷新tableview
, 最后回调自定义操做弹出了alert
框, 因为都是主线程操做, 也不会有线程安全的问题.
//
// Router.swift
// RouterPatterm
//
// Created by 双泉 朱 on 17/4/12.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
import UIKit
class Router {
static let shareRouter = Router()
var params: [String : Any]?
var routers: [String : Any]?
fileprivate let map = ["J1" : "Controller"]
func guardRouters(finishedCallback : @escaping () -> ()) {
Http.requestData(.get, URLString: "http://localhost:3001/api/J1/getRouters") { (response) in
guard let result = response as? [String : Any] else { return }
guard let data:[String : Any] = result["data"] as? [String : Any] else { return }
guard let routers:[String : Any] = data["routers"] as? [String : Any] else { return }
self.routers = routers
finishedCallback()
}
}
}
extension Router {
func addParam(key: String, value: Any) {
params?[key] = value
}
func clearParams() {
params?.removeAll()
}
func push(_ path: String) {
guardRouters {
guard let state = self.routers?[path] as? String else { return }
if state == "app" {
guard let nativeController = NSClassFromString("RouterPatterm.\(self.map[path]!)") as? UIViewController.Type else { return }
currentController?.navigationController?.pushViewController(nativeController.init(), animated: true)
}
if state == "web" {
let host = "http://localhost:3000/"
var query = ""
let ref = "client=app"
guard let params = self.params else { return }
for (key, value) in params {
query += "\(key)=\(value)&"
}
self.clearParams()
let webViewController = WebViewController("\(host)\(path)?\(query)\(ref)")
currentController?.navigationController?.pushViewController(webViewController, animated: true)
}
}
}
}
复制代码
因为线上真实项目的路由涉及公司业务, 这里就经过个人一个小demo
进行讲解, router
的本质就是一个映射, 首先router
类是一个单例, 须要有添加和删除参数的接口, 以及能够区分是native
和h5
的设计, 以及以前讲到的降级, 不用看一些三方库的设计多么酷炫, 究其本质仍是对于"\(host)\(path)?\(query)\(ref)"
进行逻辑拆分, 使用路由的好处是当使用cocoapod
私有库组件化的时候, 彻底避免了多控制器之间的耦合.
#import <objc/runtime.h>
@interface MySafeDictionary : NSObject
@end
static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;
@implementation MySafeDictionary
+ (void)swizzlling {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
kMySafeLock = [[NSLock alloc] init];
});
[kMySafeLock lock];
do {
if (kMySafeOriginalIMP || kMySafeSwizzledIMP) break;
Class originalClass = NSClassFromString(@"__NSDictionaryM");
if (!originalClass) break;
Class swizzledClass = [self class];
SEL originalSelector = @selector(setObject:forKey:);
SEL swizzledSelector = @selector(safe_setObject:forKey:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
if (!originalMethod || !swizzledMethod) break;
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);
kMySafeOriginalIMP = originalIMP;
kMySafeSwizzledIMP = swizzledIMP;
class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
} while (NO);
[kMySafeLock unlock];
}
+ (void)restore {
[kMySafeLock lock];
do {
if (!kMySafeOriginalIMP || !kMySafeSwizzledIMP) break;
Class originalClass = NSClassFromString(@"__NSDictionaryM");
if (!originalClass) break;
Method originalMethod = NULL;
Method swizzledMethod = NULL;
unsigned int outCount = 0;
Method *methodList = class_copyMethodList(originalClass, &outCount);
for (unsigned int idx=0; idx < outCount; idx++) {
Method aMethod = methodList[idx];
IMP aIMP = method_getImplementation(aMethod);
if (aIMP == kMySafeSwizzledIMP) {
originalMethod = aMethod;
}
else if (aIMP == kMySafeOriginalIMP) {
swizzledMethod = aMethod;
}
}
// 尽量使用exchange,由于它是atomic的
if (originalMethod && swizzledMethod) {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
else if (originalMethod) {
method_setImplementation(originalMethod, kMySafeOriginalIMP);
}
else if (swizzledMethod) {
method_setImplementation(swizzledMethod, kMySafeSwizzledIMP);
}
kMySafeOriginalIMP = NULL;
kMySafeSwizzledIMP = NULL;
} while (NO);
[kMySafeLock unlock];
}
- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
if (anObject && aKey) {
[self safe_setObject:anObject forKey:aKey];
}
else if (aKey) {
[(NSMutableDictionary *)self removeObjectForKey:aKey];
}
}
@end
复制代码
对于AOP
这种, 我截取了一位大佬博客中的代码, 能够看到的是, 当多线程的时候, 咱们只须要在hook
的时候进行加锁和解锁保持线程安全就能够了, 固然也可使用Aspects
这个库来简化hook
操做,毕竟AOP
这块是要看业务逻辑的, 并不能一律而论.
//
// UIViewController+hook.m
// SQTemplate
//
// Created by 朱双泉 on 23/11/2017.
// Copyright © 2017 Doubles_Z. All rights reserved.
//
#import "UIViewController+hook.h"
#import "CurrentViewController.h"
#import <Aspects.h>
@implementation UIViewController (hook)
+ (void)load {
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
kCurrentViewController = aspectInfo.instance;
} error:NULL];
}
@end
复制代码
最经常使用的AOP
就是hook
生命周期来获取当前控制器, 经过一个全局变量能够全方位获取, 以上代码就是经过AOP
模式进行面向切片编程, 避免须要继承一个基类而带来的强耦合.
所谓工欲善其事必先利其器, 上面的架构设计虽好, 但要让其余同窗模仿写法实在是太麻烦了, 也会致使抵触情绪, 但为了咱们以前代码规范的目标, 架构设计的执行也是志在必行, 这时咱们就须要进行代码自动生成的工做.
所谓的代码生成究其本质就是字符串替换, 就是将可变的字符串替换模板中的标记, ES6
中的模板字符串${变量}
也是这个道理.
咱们来看一下模板:
//
// InterfaceTemplate.h
// SQTemplate
//
// Created by 双泉 朱 on 17/5/5.
// Copyright © 2017年 Doubles_Z. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol <#Root#><#Unit#>ModelInterface <NSObject>
<#ModelInterface#>
@end
@protocol <#Root#><#Unit#>ViewModelInterface <NSObject>
@optional
@property (nonatomic,strong) id<<#Root#><#Unit#>ModelInterface> model;
@optional
- (void)initializeWithModel:(id<<#Root#><#Unit#>ModelInterface>)model <#InitializeInterface#>completion:(void(^)())completion;
<#ViewModelInterface#>
@end
@protocol <#Root#><#Unit#>ViewInterface <NSObject>
@property (nonatomic,weak) id<<#Root#><#Unit#>ViewModelInterface> <#unit#>ViewModel;
@property (nonatomic,weak) id<<#Root#><#Unit#>ViewModelInterface> <#unit#>Operator;
@end
复制代码
并进行读写操做:
//
// SQFileParser.m
// SQBuilder
//
// Created by 朱双泉 on 17/08/2017.
// Copyright © 2017 Castie!. All rights reserved.
//
#import "SQFileParser.h"
@implementation SQFileParser
+ (NSDictionary *)parser_plist_r {
NSBundle * bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"builder.bundle" ofType:nil]];
NSDictionary * config = [NSDictionary dictionaryWithContentsOfFile:[bundle pathForResource:@"config/config.plist" ofType:nil]];
NSMutableDictionary * plist = [NSDictionary dictionaryWithContentsOfFile:[bundle pathForResource:[NSString stringWithFormat:@"config/%@.plist",config[@"builderSource"]] ofType:nil]].mutableCopy;
[config enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[plist setObject:obj forKey:key];
}];
return plist;
}
+ (void)parser_rw:(NSString *)path code:(NSString *)code filename:(NSString *)filename header:(NSString *)header parameter:(NSMutableArray *)parameter {
NSString * arch = [[filename componentsSeparatedByString:@"."]firstObject];
NSString * suffix = [[filename componentsSeparatedByString:@"."]lastObject];
NSString * filename_r = [NSString stringWithFormat:@"%@Template.%@", arch,suffix];
NSString * filename_w = [NSString stringWithFormat:@"%@/%@%@.%@", path,header,arch,suffix];
NSString * template = [SQFileParser parser_r:filename_r code:[code lowercaseString]];
[[SQFileParser replaceThougth:template parameter:parameter] writeToFile:filename_w atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
+ (NSString *)parser_r:(NSString *)filename code:(NSString *)code {
NSBundle * bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"builder.bundle" ofType:nil]];
return [NSMutableString stringWithContentsOfFile:[bundle pathForResource:[NSString stringWithFormat:@"template/%@/%@", code, filename] ofType:nil] encoding:NSUTF8StringEncoding error:nil];
}
static NSString * code;
+ (NSString *)replaceThougth:(NSString *)templete parameter:(NSMutableArray *)parameter {
__block NSString * temp = templete;
[[parameter firstObject] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
temp = [templete stringByReplacingOccurrencesOfString:key withString:obj];
}];
[parameter removeObjectAtIndex:0];
if (parameter.count) {
[SQFileParser replaceThougth:temp parameter:parameter];
} else {
code = temp;
}
return code;
}
@end
复制代码
关于生成工具的开发以前有一篇详细论述了, 这里就不过多赘述了. 点击跳转
我司已经经过读取表格进行生成iOS
和Android
两端的代码, 保持两端逻辑相同, 但因为表格的设计与公司业务及我的习惯相关, 这部分代码不予公开, 请谅解.
能够看到生成的文件经过文件夹的形式存在, 只须要将文件夹导入项目中便可当即得到以前所设计的架构.
最后我将架构demo
以及工具
放在了github上, 关于上面router
降级这块能够点击这里下载
git clone
| download
后打开SQTemplate
工做空间, 就可以看到两个项目.
SQBuilder
是生成工具的项目.
配置生成工具的接口文档字段后, 点击Run
便可生成代码, 显示在桌面.
SQTemplate
是模板生成后在项目中使用的demo
.
能够看到上述全部的架构设计模式的简单引用, 点击秒速五厘米的图片, 能够经过路由跳转到下一页面.
图片下面的数据是经过koa
服务返回的数据, 下载coderZsq.target.swift中的RouterPattern
中的/server/RouterPattern
, cd
进去后执行npm start
, 便可开启服务. 固然你须要Node
, 环境和webpack
的全局环境.
若是你须要查看了解Router
部分, 能够经过coderZsq.target.swift这个项目进行学习交流.
app/RouterPattern
直接双击打开项目便可web/RouterPattern
cd
进去 npm run dev
server/RouterPattern
cd
进去 npm start
具体能够查看Hybird 搭建客户端实时降级架构系列.
注意!!! 使用git
上传时会经过.gitignore
忽略上传文件, 因此pull
下来记得pod install
| npm install
, 记得pod install
| npm install
记得pod install
| npm install
, 重要的事情说三遍!!
以上就是我对于移动端架构初探的心得, 期待与各位大佬进行交流.
🌟 项目源码 请点这里🌟 >>> 喜欢的朋友请点喜欢 >>> 下载源码的同窗请送下小星星 >>> 有闲钱的壕们能够进行打赏 >>> 小弟会尽快推出更好的文章和你们分享 >>> 你的激励就是个人动力!!