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层显示,遵守如下约定:
- V层触发请求 -> P层传递调用 -> I 层网络获取数据 -> P层处理数据 -> V层显示数据
- 通过
**do**
标识定义在 P层调用 -> I层中具体实现逻辑(通常为调用底层的网络请求服务,数据库服务获取数据) - 通过
**handle**
标识从 I层获取数据后 -> P层处理数据的函数 - 通过
**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") }}
总结:
以后要给某一个类扩展功能,可采取下面步骤
-
定义一个前缀类型(如上面的hcc等)
-
定义一个协议(protocol)
-
遵守该协议即可
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。期待你的加入!!!