Viper has nothing to do with familiar MVC, it is a new architecture, if you are not familiar with Viper, you can go to www.objc.io/issues/13-a…

The company is currently using a variant of the Viper architecture and feels that it has a good granularity of responsibility and complies with the SOLID principles of the design pattern (single, open, Closed, Richter substitution, interface isolation, dependency inversion) to make the application more robust and maintainable.

This blog is mainly about my project using a modified version of Viper architecture – [Bviper].

I. Viper architecture

The most used Viper architecture diagram is shown below:

  • Interators – include business logic about data and network requests, such as creating an Entites or fetching data from a server. Services and data managers are required, but they are not considered modules within the Viper architecture, but external dependencies.
  • Presenter – Contains business logic at the UI level and method calls at the interaction level that are responded to by user input (via the basic model object used by Interator).
  • Entity – Contains the base model object used by Interator.
  • View – Displays what is being told by the Presenter and relays user input back to the Presenter.

If it’s not clear, read the following sections carefully:

- View Provides a complete View. Responsible for View composition, layout, and update The interface that provides View updates to presenters send view-related events to presenters - presenters receive and process events from the View and invoke business logic to Interactor requests Provides data in the View to the Interactor receives and processes data from the Interactor callback events that tell the View to update and jump from the Router to another view-Router provides jump between views, - Interactor maintains major business logic functions, provides existing business use cases to Presenter, maintains, obtains, and updates Entity when business related events occur, handles events, And notify presenters of the same data Model as ModelCopy the code

From the above, I feel that Viper is well decoupled.

Viper -Bviper

The company sees the benefits of Viper and the number of staff is also increasing. Several people maintain a project. The company also has several projects, and the whole team is constantly increasing. In order to better maintain the code, the company team decided to make minor changes to Viper, eliminate redundant classes, and take a middle ground between decoupling and multiple classes. The following will focus on the viper variant of the company’s project!

2.1 template

Viper template generation relies on Generamba, installation configuration reference

https://github.com/rambler-digital-solutions/Generamba
Copy the code

Template Configuration Address

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

2.2 File content and structure

2.3 Usage Specifications

Below is a summary of the use of the BViper template

  • Func config[XXX]Scene(XXX: Type) function defined in each Scene

  • [Mandatory] Reverse value transfer (callback) between consecutive scenarios must be implemented through the xxxModuleOutput protocol

  • “Mandatory”For inter-scene jumps, the function is defined asfunc open[xxx]Scene

  • ** [mandatory] ** Network requests are triggered from Layer V to data response and then displayed at Layer V, following the following conventions:
  1. Layer V triggers the request -> Layer P passes the call -> Layer I gets the data -> Layer P processes the data -> Layer V displays the data
  2. through **do** The identity defines the implementation logic in the P layer invocation -> I layer (usually to invoke the underlying network request service, database service to fetch data)
  3. through **handle** Identifies the functions that process the data from layer I -> layer P after it has been retrieved
  4. through **did** Indicates that the data has been processed by layer P -> layer V display

  • P-layer services: receives data from other scenarios, invokes methods of Layer I to obtain data, processes data returned by layer I, and invokes Layer V to display data or error messages after data processing.

expand

Specification for P layer data processing

Using guard statements to determine code and subcode, and subcode comparisons, does not allow writing strings. Enumeration types should be defined as much as possible, using switch for enumeration comparisons

Examples of errors: Let (code, subCode) = (response.code, response.statusCode) SubCode == "1400B00" else {handleErrorMessage(response.message) return} Correct Example: enum ThirdLoginSubcodeEnum: String {case loginSuccess = "0F03200" // Successful login case uniqueIdEmpty = "0F03201" // Unique identifier is empty case noBindPhone = "0F03202" // Unbound mobile phone number case forbidLogin = "0F03203" // login forbidden case loginError = "0F03204" // loginError} 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: ...... }Copy the code
  • Layer I service: obtains network data and sends it to Layer P for processing

As follows:

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) }Copy the code
  • Layer V business: trigger the request to obtain data and obtain the data display processed by layer P

As follows:

extension ViperDemoViewController: ViperDemoViewInput {func didUserLogin() {// Show data or other business} func showErrorMessage(_ message: String) {// Error}}Copy the code

[Mandatory] Data acquisition rule of layer V: The method of layer P is not passed to layer V as a parameter. Instead, layer V obtains data of layer P through Protocol

Extension ViperDemoPresenter: ViperDemoPresenterView {var userInfos: UserInfoEntity? Protocol ViperDemoViewOutput {var userInfos: UserInfoEntity? {get} }Copy the code
  • [Mandatory] Layer E rule: Layer E is the data model

If there are only 1-2 entities, you can write it on layer I.

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

// MARK: - Interactor
class ViperDemoInteractor {
}
Copy the code

If there are more than one, separate layer E files are created and entity classes are placed in one file

// MARK: - ImageEntity
class ImageEntity { 
}

// MARK: - InfoEntity
class InfoEntity {
}
Copy the code

The above describes the requirements of the company’s code, but also focuses on the Viper version of Bviper deformation of each module function!

2.4 Use of BViper template

2.4.1 Establishing template code

 generamba gen AgentDetail bviper
Copy the code

Then create the following template:

Now let’s focus on how does the company code work

2.4.2 Protocol agreement

//MARK: - ModuleProtocol /** External incoming value * methods for communication OuterSide -> AgentDetail * define the capabilities of AgentDetail  */ protocol AgentDetailModuleInput: class { func configeListAgentIdDetail(id: Int)} /** * methods for communication AgentDetail -> OuterSide * tells the caller what is changed */ / AgentDetailModuleOutput: class { func reversePassUpdateCommentNumber(entity: DetailEntity) } //MARK: -sceneProtocol /** * Methods for communication PRESENTER -> VIEW Protocol AgentDetailViewInput: class {func didGetAgenttDetail() func showErrorMessage(_ message: } /** * Methods for communication VIEW -> PRESENTER */ / Triggering network requests and switching modules Protocol AgentDetailViewOutput {var entity: DetailEntity? {get} func getAgentListDetail(id: Int) / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- a push -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / open the dealer details page func openBrokerDetailScene (brokerId: /** * Methods for communication PRESENTER -> INTERACTOR */ / The network to I layer begins to trigger the network request protocol AgentDetailInteractorInput {func doGetAgentListDetail (id: } /** * methods for communication INTERACTOR -> PRESENTER */ / Layer I -> layer P callback layer I network request to layer P protocol AgentDetailInteractorOutput: class { func handleAgentListDetail(response: BLResponseEntity, isDB: Bool) func handleErrorMessage(_ message: String) }Copy the code

View – ViewController 2.4.2

Trigger the network request in viewDidLoad

Then the data callback in P->V

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

Entity: Self.output. entity: self.output.entity

Presenter layer

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" // Illegal deletion} extension AgentDetailPresenter {var nav: UINavigationController? { return self.transitionHandler.navigationController } } //MARK: - AgentDetailPresenterView extension AgentDetailPresenter: AgentDetailPresenterView{ var entity: DetailEntity? {pEntity} / / back propagation value func didUpdateAgentListCommentNumbers (entity: DetailEntity) {self. The outer? . ReversePassUpdateCommentNumber (entity: the entity)} / / web request to the I layer func getAgentListDetail (id: Int) {interactor. DoGetAgentListDetail (id: id)} / / jump to other modules func openBrokerDetailScene (brokerId: Int) { let (vc, input) = BrokerDetailModuleBuilder.setupModule() input.configBrokerDetailScene(brokerId: brokerId) nav? .pushViewController(vc, animated: true) } } //MARK: - AgentDetailPresenterInteractor extension AgentDetailPresenter: AgentDetailPresenterInteractor {/ / I layer - > 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) } }Copy the code

Specific P layer to do what things, said above, can be compared to see!

Interactor layer: Network requests and results are called back to the P layer

/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 // Call back to the P layer to handle data self? .output? HandleAgentListDetail (response: response, isDB: false) // Cache data HCCDBManager. AddCache (key: dbKey, response: response) }, failure: {[weak self] (message) in self? .output? .handleErrorMessage(message) }, completed: nil) } }Copy the code

Entity layer: Entity layer

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?
}
Copy the code

One thing not shown here is router-cross-module jumps.

Router

Here is an example

If the module-grade Module- > module-broker enters the detail page of the Broker, it takes the target-action method of CTMediator

Start processing jump trigger requests in module-grade:

Func openBrokerDetailScene () {/ / CTMediator way, to get across a scene of VC guard let brokerDetailVC = CTMediator. SharedInstance ()? .Broker_DetailVC(brokerId: brokerId, callback: { (_) in }) else { return } nav? .pushViewController(brokerDetailVC, animated: true) }Copy the code

Then click in the CTMediator. SharedInstance ()? .Broker_DetailVC(brokerId: brokerId, callback: { (_) in })

The Broker code inside the Router

@objc func Broker_DetailVC(brokerId: Int, callback:@escaping (Bool) -> Void) -> UIViewController? {/ / brokerVC parameters let params = [" brokerId ": brokerId," callback ": callback, kCTMediatorParamsKeySwiftTargetModuleName: ModuleName_Broker ] as [AnyHashable : Any] // Guard let viewController = self. PerformTarget (Target_Broker, Action: "brokerDetailVC", Params: params, shouldCacheTarget: false) as? UIViewController else { return nil } return viewController }Copy the code

Then look at the Action_brokerDetailVC method in Broker_Mediatorde of Moudle_Broker

@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
  }
Copy the code

As for the target-action method of CTMediator, I will not explain it here. There will be a special article in nuggets, or I hope I will explain the pros and cons of component-based development in the future.

3, Bviper improvement [only relative]- different opinions are welcome

As you can see, the last Router above is the CTMediator target-Action method, but there are a lot of things in it that developers are not comfortable with, such as the use of hard-coded brokerDetailVC strings. For Swift, Apple officially has plenty of ways for developers to use structs and protocols wherever possible. Here’s a look at the idea of protocol-oriented programming -POP:

3.1 POP thought

Example 1

How to extract the public method run method of BVC and DVC?

Solution:

protocol Runnable {
    func run()
}

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

class BVC: UIViewController, Runnable{}
class DVC: UITableViewController, Runnable{}
Copy the code

Pay attention to POP

  • Create protocols in preference to parent (base) classes
  • Prioritize value types (struct, enum) over reference types (class)
  • Smart use of protocol to expand functionality
  • Don’t apply protocols for protocol oriented purposes

Example 2: Implement prefix effects using protocols

The realization of “1234 dafdaf1234”. HCC. NumberCount

(Hcc. numberCount is an attribute of the HCC class.)

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)
Copy the code

The string extension series has been completed above, and it has been very good and elegant to solve the problem, but if you extend a function relative to strings, it is OK!

If you want to extend a similar approach to arrays, you need to add array attributes, initialization and expand array functions in HCC, and you will find too much redundant code, which is not encapsulated enough and not general enough

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()Copy the code

But if you’re going to add a Dog class again, you want it in the Dog class as well

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

This code, if added, would still make the code a bit redundant, so you see the benefit of POP – it’s protocol-oriented programming, pulling out common places (protocols can only declare things, and if you want to extend things, add them in Extension).

Struct HCC<Base> {var Base: Base init(_ Base: Protocol HCCCompatible {}extension HCCCompatible {var HCC: HCC<Self> {HCC(Self)} static var HCC: HCC<Self>.Type {HCC<Self>. HCCCompatible {} 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") }}Copy the code

Conclusion:

To extend functionality to a class later, take the following steps

  1. Define a prefix type (HCC, etc., above)

  2. Define a protocol

  3. Just abide by the agreement

3.2 Router in BViper is replaced with Protocol

The Broker module is used to explain how to unlock the Router with Protocol. Through the Router. The broker. BrokerDetailVC

1. Define common protocols and create basic protocols for each module

Public protocol Routable {// public protocol Routable}Copy the code

2. Create module protocols. Follow the basic protocols and define module methods

Public Protocol Broker_Routable: Routable {/// broker details page /// -parameters: /// -brokerID: Dealer ID /// -CollectionHandle: Eliminate or Add Func brokerDetailVC(brokerId: Int, collectionHandle: @escaping ((_ isCandel: Bool) -> Void)) -> UIViewController }Copy the code

3. Since the router.broker. Method is required, share is used to comply with the protocol singleton of each module

Public class Router {static let shared: Router = Router() private init() {} Broker_Routable { shared as! Broker_Routable } }Copy the code

4. Method of implementing the protocol

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
    }
}
Copy the code

Call as follows:

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

The above is just a dealer module, if for the whole project the broker passes through Router. Broker and Grade passes through

Router.grade, complainCenter through Router.complainCenter… Using Share’s singleton to comply with the protocol methods of the various submodules, you can get to Share as Grade, complainCenter, and so on

public class Router { static let shared: Router = Router() private init() {} public static var home: Home_Routable { shared as! Public static var broker: Broker_Routable {shared as! Broker_Routable} /// module public static var grade: Grade_Routable {shared as! Public static var complainCenter: ComplainCenter_Routable {shared as! ComplainCenter_Routable} /// my module public static var mine: Mine_Routable {shared as! Public static var account: Account_Routable {shared as! Account_Routable } }Copy the code

Using protocol solves the method of hard-coded CTMediator, and also makes good use of swift’s programming idea. In the future, WE will gradually maintain and expand better Bviper architecture. For example, the data processing of Interator layer and E layer will be handed over to additional dataManager, which is a very good way of processing.

This Bviper is mainly about the framework and arrangement used by the company, which is combined with the actual application of the company’s projects. If all Viper architectures are used, folders may be too many, which is not conducive to maintenance, so I will integrate them as above. Correction is welcome!!