iOS Viper架构变形及优化

1,432

Viper和熟知的MVC没有啥关系,是一个全新的架构,如果大家对于Viper不是很熟悉,可以去阅读一下www.objc.io/issues/13-a… 

目前公司就在使用Viper架构的变形版,觉得职责粒度划分很好,符合设计模式的SOLID原则【单一原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则】让程序更加健壮和维护。

本篇博客主要是讲述本人项目使用Viper架构的变形版-【Bviper】。

一、Viper架构思想

使用最多的viper架构图就是如下图:

  • 交互器(Interator)-包括关于数据和网络请求的业务逻辑,例如创建一个实体【Entites】或者从服务器获取数据,需要使用服务和数据管理器,但是他们并不被认为是Viper架构内的模块,而是外部依赖。
  • 展示器(Presenter)-包含UI层面的业务逻辑以及交互器层面的方法调用,用户输入做出反应(通过Interator使用的基本模型对象)。
  • 实体(Entity)-包含Interator使用的基本模型对象。
  • 视图(View)-显示Presenter告知的内容,并将用户输入中继回Presenter。

如果还不是很清晰,可以认真读取下面各部分的职责:

- View
提供完整的视图,负责视图的组合、布局、更新
向Presenter提供更新视图的接口
将View相关的事件发送给Presenter

- Presenter
接收并处理来自View的事件
向Interactor请求调用业务逻辑
向Interactor提供View中的数据
接收并处理来自Interactor的数据回调事件
通知View进行更新操作
通过Router跳转到其他View

- Router
提供View之间的跳转功能,减少了模块间的耦合
初始化VIPER的各个模块

- Interactor
维护主要的业务逻辑功能,向Presenter提供现有的业务用例
维护、获取、更新Entity
当有业务相关的事件发生时,处理事件,并通知Presenter

- Entity
和Model一样的数据模型

通过上面的了解,感觉到Viper已经很好地达到解耦啦!

二、Viper变形版-Bviper

公司也就是看到Viper的好处以及目前人员也在不断地增加,几个人维护一个项目,公司也有几个项目,整个团队在不断的增大。为了便于更好地维护代码,觉得对Viper进行稍微改动,去除了多余的类,在解耦和多类的取舍下,公司团队采取了中庸的做法-各有取舍。下面将着重讲解公司项目的viper变形版!

2.1 模板

viper模板的生成依赖于Generamba ,安装配置参考

https://github.com/rambler-digital-solutions/Generamba

模板配置地址

### Templates
catalogs:
- "https://git***.***.com/iOS/bviper.git"
templates:
- {name: bviper}

2.2 文件内容和结构

2.3 使用规范

下面是总结的BViper模版的使用

  • 【强制】连续的场景间正常传值,必须要通过xxxModuleInput协议完成,每一个场景内函数定义func config[xxx]Scene(xxx: Type)

  • 【强制】连续的场景间反向传值(回调),必须要通过xxxModuleOutput协议完成

  • 【强制】对于场景间跳转,函数定义为func open[xxx]Scene

  • **【强制】**网络请求从V层触发到数据响应然后再到V层显示,遵守如下约定:
  1. V层触发请求 -> P层传递调用 -> I 层网络获取数据 -> P层处理数据 -> V层显示数据
  2. 通过 **do** 标识定义在 P层调用 -> I层中具体实现逻辑(通常为调用底层的网络请求服务,数据库服务获取数据)
  3. 通过 **handle** 标识从 I层获取数据后 -> P层处理数据的函数
  4. 通过 **did** 标识数据已经过 P层处理 -> V层显示

  • P层业务:接受其他场景传入的数据,调用I层方法获取数据,处理I层返回的数据,数据处理完后调用V层展示数据或者错误提示。

拓展

P层数据处理时的规范

使用guard语句判断code和subcode,以及subcode 的比较不允许写字符串,应该尽量定义枚举类型,使用switch进行枚举比较

错误示例:
let (code, subCode) = (response.code, response.statusCode)
 /// 这个的subCode 使用了直接和字符串比较的方式(不允许)
 guard code == .success, subCode == "1400B00" else {
     handleErrorMessage(response.message)
     return
 }


正确示例:
enum ThirdLoginSubcodeEnum: String {
    case loginSuccess = "0F03200"  // 登录成功
    case uniqueIdEmpty = "0F03201" // 唯一标识为空
    case noBindPhone = "0F03202"   // 未绑定手机号码
    case forbidLogin = "0F03203"   // 禁止登录
    case loginError = "0F03204"    // 登录错误
}

let (code, statusCode) = (response.code, statusCode)
guard code == HttpRequestResult.success else {
    self.handleErrorMessage(response.message)
    return
}

/// 使用枚举比较subcode 
let statusEnum = ThirdLoginSubcodeEnum(rawValue: statusCode)
switch statusEnum {
case . loginSuccess:
.....
default: 
......
}
  • I层业务:获取网络数据传给P层处理

如下:

func doUserLogin(userName: String, password: String) {
    let api = LocaleManager.shared.apiUtils.Home.UserLogin
    var params = [String: Any]()
    params["userName"] = userName
    params["pwd"] = password

    let request = BLRequestEntity()
    request.api = api
    request.extraQueryParams = params
    BLHttpManager.shared.post(request: request, success: { (response) in
        self.output?.handleUserLogin(response: response)
    }, failure: { (message) in
        self.output?.handleErrorMessage(message)
    }, completed: nil)
}
  • V 层业务: 触发获取数据的请求,获取 P 层处理好的数据展示

如下:

extension ViperDemoViewController: ViperDemoViewInput {
    func didUserLogin() {
         // 展示数据或其他业务
    }

    func showErrorMessage(_ message: String) {
        // 提示错误
    }

}

【强制】V 层的数据获取规则:不通过 P层 的方法作为参数传给 V 层,而是由 V层 通过 Protocol 获取 P层 的数据

// MARK: - P 层返回数据
extension ViperDemoPresenter: ViperDemoPresenterView {
var userInfos: UserInfoEntity? {
    return pUserInfo
 }
}

/// View -> Presenter ( V层 通过 Protocal 获取 P层 数据)
protocol ViperDemoViewOutput {
   var userInfos: UserInfoEntity? {get}
}
  • 【强制】E 层规则:E 层为数据模型

如果只有1-2个实体,可以写在I层上【省一个文件】

/ MARK: - Entity
class UserInfoEntity {
    var uid: Int = 0
    var userName: String = ""
    var userImage: String?
}

// MARK: - Interactor
class ViperDemoInteractor {
}

如果有多个则单独创建E层文件,将实体类放在一个文件中

// MARK: - ImageEntity
class ImageEntity { 
}

// MARK: - InfoEntity
class InfoEntity {
}

上面讲述了公司代码的要求,也着重讲解了Viper变形版Bviper的各个模块功能!

2.4 BViper模版使用

2.4.1 建立模版代码

 generamba gen AgentDetail bviper

然后建立模版如下:

下面着重看下公司代码如何践行的

2.4.2  Protocol协议

//MARK: - ModuleProtocol
/** 外部传入值
 *  methods for communication OuterSide -> AgentDetail
 *  define the capabilities of AgentDetail
 */
protocol AgentDetailModuleInput: class {
    func configeListAgentIdDetail(id: Int)
}

/**
 *  methods for communication AgentDetail -> OuterSide
 *  tell the caller what is changed
 */
//内部向外回调
protocol AgentDetailModuleOutput: class {
    func reversePassUpdateCommentNumber(entity: DetailEntity)
}


//MARK: - SceneProtocol
/**
 *  methods for communication PRESENTER -> VIEW
 */
// P层 -> V层: 传递给View结果和展示
protocol AgentDetailViewInput: class {
    
    func didGetAgenttDetail()
    func showErrorMessage(_ message: String)
}

/**
 *  methods for communication VIEW -> PRESENTER
 */
// V层 -> P层: 触发网络请求,以及跳转模块的触发
protocol AgentDetailViewOutput {
    
    var entity: DetailEntity?{get}
    
    func getAgentListDetail(id: Int)
    //----------------- push ---------------------------------
    //打开交易商详情页
    func openBrokerDetailScene(brokerId: Int)
}

/**
 *  methods for communication PRESENTER -> INTERACTOR
 */
// P层 -> I层: 将网络传给I层开始触发网络请求
protocol AgentDetailInteractorInput {
    
    func doGetAgentListDetail(id: Int)

}

/**
 *  methods for communication INTERACTOR -> PRESENTER
 */
// I层 -> P层 将I层的网络请求回调给P层
protocol AgentDetailInteractorOutput: class {
    
    func handleAgentListDetail(response: BLResponseEntity, isDB: Bool)
    
    func handleErrorMessage(_ message: String)
}

2.4.2 View-ViewController

在viewDidLoad里面触发网络请求

然后在P->V的数据回调

func didGetAgenttDetail() {
        self.view.removePlaceholder()
        self.hcc_hideActivity()
        self.tableView.reloadData()
 }

至于如何取数据entity,可以通过self.output.entity方式

Presenter层

import UIKit

typealias AgentDetailPresenterView = AgentDetailViewOutput
typealias AgentDetailPresenterInteractor = AgentDetailInteractorOutput

class AgentDetailPresenter {
    weak var view: AgentDetailViewInput!
    weak var transitionHandler: UIViewController!
    var interactor: AgentDetailInteractorInput!
    var outer: AgentDetailModuleOutput?
    
    fileprivate var isRefreshNews: Bool = false
    
    fileprivate var pEntity: DetailEntity?
}

enum AgentDetailSubcodeEnum: String {
    case commentNoteSuccess = "0"
    case agentListSoldOut = "0F02D03"  // 违规下架
}

extension AgentDetailPresenter {
    var nav: UINavigationController? {
        return self.transitionHandler.navigationController
    }

}

//MARK: - AgentDetailPresenterView
extension AgentDetailPresenter: AgentDetailPresenterView{
    var entity: DetailEntity? {
         pEntity
    }
    
    //反传值
    func didUpdateAgentListCommentNumbers(entity: DetailEntity) {
        self.outer?.reversePassUpdateCommentNumber(entity: entity)
    }
    
    //网络请求传给I层
    func getAgentListDetail(id: Int) {
        interactor.doGetAgentListDetail(id: id)
    }
    
    //跳转到其他模块
    func openBrokerDetailScene(brokerId: Int) {
        let (vc, input) = BrokerDetailModuleBuilder.setupModule()
        input.configBrokerDetailScene(brokerId: brokerId)
        nav?.pushViewController(vc, animated: true)
    }
    
}

//MARK: - AgentDetailPresenterInteractor
extension AgentDetailPresenter: AgentDetailPresenterInteractor {
    //I层 -> P层
    func handleAgentListDetail(response: BLResponseEntity, isDB: Bool) {
        let (code, subcode) = (response.code, response.statusCode)
        guard code == HttpRequestResult.success, subcode == AgentDetailSubcodeEnum.commentNoteSuccess.rawValue else {
            if isDB {return}
            self.handleErrorMessage(response.message)
            return
        }
        
        if let _ = DetailEntity.deserialize(from: response.bodyMessage) {
            self.view.didGetAgenttDetail()
        }
        
    }
    
    func handleErrorMessage(_ message: String) {
        self.view.showErrorMessage(message)
    }

}

具体P层做的什么事情,上面说啦,可以对照着看!

Interactor层: 网络请求以及结果回调给P层

/MARK: - Interactor
class AgentDetailInteractor{
    weak var output: AgentDetailInteractorOutput?
}

extension AgentDetailInteractor: AgentDetailInteractorInput {
    func doGetAgentListDetail(id: Int) {
        let api =  HCCApi.broker.AgentListDetailApi
        var params = [String: Any]()
        params["id"] = id
        
        let request = BLRequestEntity()
        request.api = api
        request.params = params
        
        // 获取缓存数据
        let dbKey = kBroker_agentDetailDBKey+"\(id)"
        HCCDBManager.loadCache(key: dbKey) { (response) in
            self.output?.handleAgentListDetail(response: response, isDB: true)
        }
          
        HCCHttpManager.shared.get(request: request, success: {[weak self] (response) in
            //回调给P层处理数据
            self?.output?.handleAgentListDetail(response: response, isDB: false)
            
            // 缓存数据
            HCCDBManager.addCache(key: dbKey, response: response)
            
        }, failure: {[weak self] (message) in
            self?.output?.handleErrorMessage(message)
                
        }, completed: nil)
    }
    
}

Entity层: 实体

class DetailEntity: HCCBaseEntity {
    var ibId: Int = 0
    var type: Int = 0
    var name: String = ""
    var abbName: String = ""
    var tempLi: Int = -1
    var onlineService: Bool = false
    var countryNo: Int = 0
    var officialQQ: String = ""
    var telphone: String = ""
    var email: String = ""
    var address: String = ""
    var website: String = ""
    var website2: String = ""
    var status: Int = 0
    var logo: String = ""
    var autStatus: Int = 0
    var establishedTime: String = ""
    var clicks: Int = 0
    var comments: Int = 0
    var countryName: String = ""
    var feature: String = ""
    var score: String = ""
    var logoFive: String = ""
   
    var ibBroker: [PlateformEntity]?
    
}

class BaseEntity: HCCBaseEntity {
    var id: Int = 0
    var category: Int = 0
    var title: String = ""
    @objc dynamic var addTime: String = "" {
        didSet{
            self.formatTime = addTime.formatDateString()
        }
    }
    var formatTime: String = ""
    var titleImages: TitleImagesEntity?
}

在这里有一个没有体现出来就是Router-跨模块的跳转.

Router

下面以一个例子说明

如果Module-Grade模块 -> Module-Broker,进入到Broker的详情页,采取的是CTMediator的Target-Action方式

在Module-Grade中开始处理跳转触发请求:

func openBrokerDetailScene() {
        //通过CTMediator方式,获取跨场景的VC
        guard let brokerDetailVC = CTMediator.sharedInstance()?.Broker_DetailVC(brokerId: brokerId, callback: { (_) in }) else {
            return
        }
        nav?.pushViewController(brokerDetailVC, animated: true)
  }

然后点击进去看CTMediator.sharedInstance()?.Broker_DetailVC(brokerId: brokerId, callback: { (_) in })

Router里面Broker代码

/// 交易商详情,callback是否取消收藏
    @objc func Broker_DetailVC(brokerId: Int, callback:@escaping (Bool) -> Void) -> UIViewController? {
        //brokerVC的参数
        let params = [
            "brokerId": brokerId,
            "callback":callback,
            kCTMediatorParamsKeySwiftTargetModuleName: ModuleName_Broker
            ] as [AnyHashable : Any]
        //通过performTarget方式
        guard let viewController = self.performTarget(Target_Broker, action: "brokerDetailVC", params: params, shouldCacheTarget: false) as? UIViewController else {
            return nil
        }
        return viewController
    }

然后再看Moudle_Broker的Broker_Mediatorde中的Action_brokerDetailVC方法

@objc func Action_brokerDetailVC(_ params: NSDictionary) -> UIViewController {
       
        let (vc, input) = BrokerDetailModuleBuilder.setupModule()
        if let brokerId = params["brokerId"] as? Int {
            input.configBrokerDetailScene(brokerId: brokerId)
        }
        if let callback = params["callback"] as? (Bool) -> Void {
            input.configCancelCollectionCallback(callback)
        }
        return vc
  }

至于CTMediator的Target-Action方式,在这个就不做讲解啦,在掘金上会有专门的文章,或者期待以后我会专门讲解组件化开发的优劣处的!

三、Bviper改进[只是相对]-欢迎不同意见

大家看到上面最后的Router是通过CTMediator的Target-Action的方式,但是大家看到没有,其实里面还是有很多的看着并不让开发者心里舒服的,比如硬编码的出现,直接字符串“brokerDetailVC”等。对于swift中,苹果官方是大量让开发者尽可能使用struct和Protocol协议的方式。下面来讲解一下面向协议编程-POP思想:

3.1 POP思想

实例1

如何将BVC、DVC的公共方法run方法抽取出来?

解决方案:

protocol Runnable {
    func run()
}

extension Runnable {
    func run() {
        print("run")
    }
}

class BVC: UIViewController, Runnable{}
class DVC: UITableViewController, Runnable{}

POP的注意点

  • 优先考虑创建协议,而不是父类(基类)
  • 优先考虑值类型(struct, enum),而不是引用类型(class)
  • 巧妙利用协议拓展功能
  • 不要为了面向协议而实用协议

实例2:使用协议实现前缀效果

实现"1234dafdaf1234".hcc.numberCount

(因为是“”.hcc,所以对字符串拓展了一个属性hcc,而hcc.numberCount又是hcc类的一个属性,所以如下)

struct HCC {
    var string: String
    init(_ str: String) {
        self.string = str
    }
    var numberCount: Int {
        var count = 0
        for c in string where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
}

extension String {
    var hcc: HCC {return HCC(self)}//传值self字符串
}

print("1234dafdaf1234".hcc.numberCount)

上面已经完成对字符串的拓展功能的系列,也已经很好的很优雅的解决了问题,但是如果相对字符串拓展一个功能的话,这就OK啦!

如果想对数组进行拓展一个类似的方法,还要在HCC里面增加array属性和初始化以及拓展Array功能,就会发现冗余代码太多,且不够封装,不够通用

struct HCC<Base> {
    var base: Base
    init(_ base: Base) {
        self.base = base
    }
}

extension String {
    var hcc: HCC<String> {HCC(self)}
}

class Person{}
extension Person {
    var hcc: HCC<Person> {HCC(self)}
}

extension HCC where Base == String {
    var numberCount: Int {
        var count = 0
        for c in base where("0"..."9").contains(c){
            count += 1
        }
        return count
    }
}

extension HCC where Base == Person {
    func run() {
        print("run")
    }
}

"1234dafdaf1234".hcc.numberCount
Person().hcc.run()

但是如果要再次增加一个Dog类,也要在Dog类中有

var hcc: HCC<String> {HCC(self)}
static var hcc: HCC<String>.Type {HCC<String>.self}

这些代码,增加其他,会导致代码还是会有点冗余,这样就发现了POP的好处-是面向协议编程,将公共的地方抽出来(协议只能声明一些东西,想扩充一些东西,就是在extension加入)

///前缀类型struct HCC<Base> {    var base: Base    init(_ base: Base) {        self.base = base    }}///利用协议扩展前缀属性protocol HCCCompatible {}extension HCCCompatible {    var hcc: HCC<Self> {HCC(self)}    static var hcc: HCC<Self>.Type {HCC<Self>.self}}

///给字符串扩展功能//让String拥有前缀属性extension String: HCCCompatible {}//给string.hcc以及String().hcc前缀扩展功能extension HCC where Base == String {    var numberCount: Int {        var count = 0        for c in base where("0"..."9").contains(c){            count += 1        }        return count    }    static func test() {        print("test")    }} class Person{}extension Person: HCCCompatible{}class Dog{}extension Dog: HCCCompatible{}extension HCC where Base == Person {    func run() {        print("run")    }}

总结:

以后要给某一个类扩展功能,可采取下面步骤

  1. 定义一个前缀类型(如上面的hcc等)

  2. 定义一个协议(protocol)

  3. 遵守该协议即可

3.2  BViper中的Router换做协议Protocol

下面我们就以Broker模块来讲解如何用Protocol解锁Router?通过Router.broker.brokerDetailVC

1. 定义公共协议,为每个模块创建基本的协议

public protocol Routable {
    // 公共协议
}

2. 创建模块协议遵守基本协议,定义模块方法

public protocol Broker_Routable: Routable {
    
    /// 交易商详情页
    /// - Parameters:
    ///   - brokerId: 交易商id
    ///   - collectionHandle: 收藏操作(取消或者添加)
    func brokerDetailVC(brokerId: Int, collectionHandle: @escaping ((_ isCandel: Bool) -> Void)) -> UIViewController
    
}

3. 因为要Router.broker.方法,使用share为了遵守各个模块的协议的单例

public class Router {
    static let shared: Router = Router()
    private init() {}
    
    /// 交易商模块
    public static var broker: Broker_Routable { shared as! Broker_Routable }
    
}

4. 实现协议的方法

extension Router: Broker_Routable {
    public func brokerDetailVC(brokerId: Int, collectionHandle: @escaping ((Bool) -> Void)) -> UIViewController {
        let (vc, input) = BrokerDetailModuleBuilder.setupModule()
        input.configBrokerDetailScene(brokerId: brokerId)
        input.configCancelCollectionCallback(collectionHandle)
        return vc
    }
}

调用如下:

let vc = Router.broker.brokerDetailVC(brokerId: id) { (_) in}
self.navigationController?.pushViewController(vc, animated: true)

上面仅仅是一个交易商模块,如果对于整个项目而言,broker通过Router.broker,Grade通过

Router.grade方式,complainCenter通过Router.complainCenter方式……使用share的单例遵守各个子模块的协议方法,就可以达到share为grade、complainCenter等

public class Router {
    static let shared: Router = Router()
    private init() {}
    

    public static var home: Home_Routable { shared as! Home_Routable}
    
    /// 交易商模块
    public static var broker: Broker_Routable { shared as! Broker_Routable }
    
    /// 模块
    public static var grade: Grade_Routable { shared as! Grade_Routable }
    
    /// 客诉中心模块
    public static var complainCenter: ComplainCenter_Routable { shared as! ComplainCenter_Routable }
    
    /// 我的模块
    public static var mine: Mine_Routable { shared as! Mine_Routable }
    
    /// 账户模块
    public static var account: Account_Routable { shared as! Account_Routable }
    
}

使用协议解决了硬编码的CTMediator的方式,也很好的使用了swift的编程思想。以后会慢慢的维护和拓展更好的Bviper架构,比如Interator层与E层的数据处理交给额外的dataManager等,都是很好的处理方式。

本篇Bviper主要讲述公司使用的框架和整理,是结合公司项目的实际运用,如果全部的Viper架构,可能文件夹过于繁多,不利于维护,所以就整合如上。欢迎大家指正!!!

机会❤️❤️❤️🌹🌹🌹

如果想和我一起共建抖音,成为一名bytedancer,Come on。期待你的加入!!!

截屏2022-06-08 下午6.09.11.png