iOS 移動端架構初探心得

Castie1發表於2017-11-25

本文作為這一系列的收尾總結, 詳細敘述了這個架構工具的設計思路以及一步步的優化, 在此也分享與你, 完整keynote可查閱github

參考連結:

本文作為以上文章系列的總結, 如何一步一步進行思考總結, 如何開發出適合自己的通用架構設計.

設計思路

iOS 移動端架構初探心得

對於架構, 移動端常見的架構設計包括MVC, MVVM, MVP等, 上圖簡要的說明了各種常見的架構之間的互動及資料傳遞方式.

iOS 移動端架構初探心得

對於MVC, MVVM, MVP這三種架構設計模式, 相信大家一定了然於心, 相關的文章也是多如繁星, 對於這些常用架構, 每個人都肯定有每個人的理解, 但這樣會導致一個問題, 就是極大的自由度導致了沒有程式碼規範, 對於移動端或者前端及後端來說, 其本質工作就是資料層和展示層的互動, 如何將資料正確安全高效的傳輸到展示層.

這裡的資料層從整個專案來說, 可以說是後端, 也就是服務端, 對於服務端開發的流程就是從資料庫獲取資料並將資料進行各種邏輯過濾作為響應返回給前端用於展示層展示, 對於java為例, 我們普通的專案就會分為controller, service, dao, pojo, vo, bo等層級設計, 就會將不同功能進行抽象, 使得程式碼更容易維護.

而作為展示層的前端, 也就是客戶端, 其實移動端在我感覺其實也是前端的一個分支, 而前端的架構通常為元件化設計, 每一個功能view對應一個元件, 而整個頁面可以通過多個元件分離進行維護, 很高效的將業務程式碼和檢視進行分離, 使得程式碼更有規範及易維護.

而對於移動端, 為什麼要在controller中寫那麼多不知所云的程式碼? 為什麼一個控制器能超過1k行? 為什麼view的邏輯回撥代理要寫在controller中? 為什麼控制器之間的引數傳遞的耦合性那麼強? 為什麼網路請求的方法隨處可見? 為什麼我們不能夠像前後端那樣有條理的控制我們的程式碼? 而讓其像脫韁的野馬難以駕馭呢?

iOS 移動端架構初探心得

為了解決這些問題, 我們需要考慮一些架構設計模式, 最先想到的就是以controller為中心的抽象, 將控制器的功能抽象到該具體負責的模組, 從圖上可以看到, controller維護了presenter, viewmodel, view, 而viewmodel又維護了model, 其中的model也可以說就是javabean完全的純資料結構, viewmodelmodel的上一層, 用於操作對應的資料, 可以看到controller將程式碼下發至下面三層, 使得各個層級各司其職.

iOS 移動端架構初探心得

剛才是站在controller的視角上來看的, 對於資料的傳輸, 這次我們站在presenter的視角上來看, 這個設計就是將viewmodel作為傳遞物件通過presenter這個中介軟體傳輸至view層, 這樣view層不僅可以拿到資料, 也可以對資料進行操作, 掌控性有所提高.

iOS 移動端架構初探心得

剛才我們站在了controllerpresenter視角上分析了架構設計的思路, 但這樣各個層級的耦合會越來越大, 從而導致專案程式碼無法分割, 這時想到了後端controllerservice之間通過介面進行互動來降低耦合, 我們是不是也可以參考這種方案通過一個protocol文件檔案來降低各個層級之間的耦合呢, 如圖所示, 將除了controller之外的其他層級進行解耦. 進行高度抽象.

iOS 移動端架構初探心得

上面我們解決了各個層級之間的耦合, 但我們怎麼解決控制器之間的耦合呢? 答案是router, 我們站在路由的視角上看, 各大控制器都是獨立存在的個體, 彼此之間的互動通過路由的對映進行互動, 這樣我們就能夠去除各大控制器之間的耦合了. 路由這個思路最先也是在前端的架構框架中看到的, 後來就有了cocoapods私有庫元件化這種整合化的解決方案, 通過路由對映能夠很好的做到模組分離, 更可以做到頁面降級, 所謂的頁面降級就是指不僅路由可以和native進行互動, 也可以和h5進行互動, 當nativeh5是一套業務邏輯的時候, native不慎出現bug我們可以請求後端介面修改資料庫將頁面直接降級至h5頁面而不用重新打包等待蘋果稽核及使用熱修復工具帶來了時間消耗. 能夠第一時間解決問題.

iOS 移動端架構初探心得

解決了上述問題, 我們就只剩下頁面的問題了, 對於現在的iOSer來說, 寫頁面幾乎是日常工作的絕大部分, 但是寫頁面, 寫業務, 當邏輯複雜的時候也會產生一系列不易維護的問題, 這時候我們就可以使用類似redux這種狀態機的模式, 將業務邏輯拆分出不同複雜的狀態, 當變數改變的時候, 觸發不同的狀態, 這樣就能夠有效的管理我們的頁面邏輯. 推薦可以看看reactredux的思路, 對這塊也會有更好的掌握.

設計思路的總結就是, 通過高度抽象進行分層, 通過介面文件進行專案層級解耦, 通過路由進行元件化及降級, 通過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中, 我們將viewmodelview, 也就是上述的資料層和展示層傳輸到 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,根據之前的設計圖, 我們將其區分為兩個模組, tableviewheaderviewtableviewcell, 其中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類是一個單例, 需要有新增和刪除引數的介面, 以及可以區分是nativeh5的設計, 以及之前講到的降級, 不用看一些三方庫的設計多麼酷炫, 究其本質還是對於"\(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模式進行面向切片程式設計, 避免需要繼承一個基類而帶來的強耦合.

生成工具

iOS 移動端架構初探心得

所謂工欲善其事必先利其器, 上面的架構設計雖好, 但要讓其他同學模仿寫法實在是太麻煩了, 也會導致牴觸情緒, 但為了我們之前程式碼規範的目標, 架構設計的執行也是志在必行, 這時我們就需要進行程式碼自動生成的工作.

所謂的程式碼生成究其本質就是字串替換, 就是將可變的字串替換模板中的標記, 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 移動端架構初探心得

我司已經通過讀取表格進行生成iOSAndroid兩端的程式碼, 保持兩端邏輯相同, 但由於表格的設計與公司業務及個人習慣相關, 這部分程式碼不予公開, 請諒解.

iOS 移動端架構初探心得

可以看到生成的檔案通過資料夾的形式存在, 只需要將資料夾匯入專案中即可立即獲得之前所設計的架構.

iOS 移動端架構初探心得

最後我將架構demo以及工具放在了github上, 關於上面router降級這塊可以點選這裡下載

如何使用

coderZsq.project.oc

iOS 移動端架構初探心得

git clone | download 後開啟SQTemplate工作空間, 就能夠看到兩個專案.

SQBuilder是生成工具的專案.

iOS 移動端架構初探心得

配置生成工具的介面文件欄位後, 點選Run即可生成程式碼, 顯示在桌面.

SQTemplate是模板生成後在專案中使用的demo.

iOS 移動端架構初探心得

可以看到上述所有的架構設計模式的簡單引用, 點選秒速五釐米的圖片, 可以通過路由跳轉到下一頁面.

圖片下面的資料是通過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, 重要的事情說三遍!!

以上就是我對於移動端架構初探的心得, 期待與各位大佬進行交流.

About:

點選下方連結跳轉!!

? 專案原始碼 請點這裡? >>> 喜歡的朋友請點喜歡 >>> 下載原始碼的同學請送下小星星 >>> 有閒錢的壕們可以進行打賞 >>> 小弟會盡快推出更好的文章和大家分享 >>> 你的激勵就是我的動力!!

相關文章