本文做爲這一系列的收尾總結, 詳細敘述了這個架構工具的設計思路以及一步步的優化, 在此也分享與你, 完整
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
, 重要的事情說三遍!!
以上就是我對於移動端架構初探的心得, 期待與各位大佬進行交流.
🌟 項目源碼 請點這裏🌟 >>> 喜歡的朋友請點喜歡 >>> 下載源碼的同窗請送下小星星 >>> 有閒錢的壕們能夠進行打賞 >>> 小弟會盡快推出更好的文章和你們分享 >>> 你的激勵就是個人動力!!