An overview of the

Recently, MBNetwork, a protocol-oriented network request library based on Alamofire and ObjectMapper, has been opened source to simplify network request operations at the business layer.

Something needs to be done

For most apps, the business layer usually cares about the following issues when making a network request:

  • How to initiate a network request from anywhere.
  • Form creation. The request address and request mode are as follows: GET/POST /…… First class……
  • Load the mask. The goal is to block UI interactions while informing the user that an action is taking place. For example, when submitting a form, display “chrysanthemum” on the submit button and disable it.
  • Display the loading progress. When downloading and uploading pictures and other resources, the user is prompted with the current progress.
  • Resumable at breakpoint. Download upload pictures and other resources error can be completed in the previous part of the operation on the basis of thisAlamofireIt can be supported.
  • Data parsing. Because the current mainstream server and client data exchange format isJSONSo let’s think about it for the time beingJSONFormat data parsing, thisObjectMapperIt can be supported.
  • Error message. When a service exception occurs, the exception information returned by the server is displayed. If the server exception message is friendly enough.
  • Success prompt. Prompt the user when the request ends normally.
  • The network is abnormal. The network exception screen is displayed. Click to resend the request.

Why is itPOPRather thanOOP

Many articles have been written about POP and OOP design ideas and their characteristics, so I’m going to skip the crap and focus on why POP was used to write MBNetwork.

  • I want to experiment with the design of everything by agreement. So the design of this library is just an extreme experiment, it doesn’t mean that this is the perfect way to design.
  • If theOOP, users need to inherit the way to obtain a class to achieve the function, if the user also needs another class to achieve the function, it will be very awkward. whilePOPIs through the extension of the protocol to achieve the function, users can follow multiple protocols at the same time, easy to solveOOPThis is a hard wound.
  • OOPInheritance gives some subclasses access to functionality they don’t need.
  • If some services need to be separated due to the increase of services,OOPSubclasses cannot inherit from more than one parent class, andPOPAfter separation, you only need to follow the multiple protocols after separation.
  • OOPInheritance is more intrusive.
  • POPThe default implementation of each protocol can be extended to reduce the learning cost of users.
  • At the same timePOPIt also allows users to customize the implementation of the protocol to ensure its high configurability.

Standing on theAlamofireThe shoulders of

A lot of people like to say that Alamofire is the Swift version of AFNetworking, but in my opinion, Alamofire is purer than AFNetworking. This is also due to the nature of the Swift language itself, as Swift developers tend to write lightweight frameworks. AFNetworking, for example, puts a lot of uI-related extensions in the framework, while Alamofire puts them in a separate extension library. Such as AlamofireImage and AlamofireNetworkActivityIndicator

And MBNetwork can be regarded as an extended library of Alamofire, so MBNetwork largely follows the design specification of Alamofire interface. On the one hand, it reduces the learning cost of MBNetwork. On the other hand, from a personal point of view, Alamofire does have a lot of special lessons to learn.

POP

The first, of course, is POP, where Alamofire makes extensive use of protocol + Extension implementations.

enum

As an important indicator of writing Swift posture correctly, Alamofire is certainly not lacking.

Chain calls

This is one of the things that makes Alamofire an elegant web framework. MBNetwork also performs full Copy at this point.

@discardableResult

Alamofire has this label in front of all methods that return a value. This is very simple because in Swift, Xcode generates an alarm if the return value is not used. This label means that the return value of the method does not generate an alarm even if it is not being used.

Of course,ObjectMapper

A large part of the reason ObjectMapper was introduced was to do error and success hints. ObjectMapper is introduced to do JSON parsing because the error message node on the server can only know if the result is correct. The reason for only doing JSON parsing is that the current mainstream server-side client data interaction format is JSON.

What needs to be mentioned here is another Alamofire extension library, AlamofireObjectMapper. From the name, it can be seen that this library does what ObjectMapper does by referring to Alamofire API specification. This library has very little code, but the implementation is very Alamofire, you can read its source code, basically know how to do custom data parsing based on Alamofire.

Note: Being plugged into ProtoBuf by @foolish amley…

Step by step

Form creation

There are three kinds of requests of Alamofire: Request, Upload and Download. These three kinds of requests have corresponding parameters, and MBNetwork abstracts these parameters into corresponding protocols. For details, see MBform.swift. This approach has several advantages:

  1. For similarheadersSuch parameters are generally globally consistent and can be specified directly in Extension.
  2. You can use the protocol name to know the form’s function, which is simple and clear.

The following is an example of the MBNetwork form protocol:

Specify the global headers argument:

extension MBFormable {
    public func headers(a)- > [String: String] {
        return ["accessToken":"xxx"]; }}Copy the code

Create a concrete business form:

struct WeatherForm: MBRequestFormable {
    var city = "shanghai"

    public func parameters(a)- > [String: Any] {
        return ["city": city]
    }

    var url = "https://raw.githubusercontent.com/tristanhimmelman/AlamofireObjectMapper/2ee8f34d21e8febfdefb2b3a403f18a43818d70a/sampl e_keypath_json"
    var method = Alamofire.HTTPMethod.get
}
Copy the code

Form protocolization may be suspected of excessive design. Those who share the same opinion can still use the corresponding interface of Alamofire to make network requests without affecting the use of other functions of MBNetwork.

Request data based on the form

The form has been abstracted into a protocol, and now you can send network requests based on the form. As mentioned earlier, you need to send network requests anywhere, and there are basically a few ways to do this:

  • The singleton.
  • Global method,AlamofireThat’s how it works.
  • Protocol extension.

MBNetwork takes the last approach. The reason is simple, MBNetwork is designed with the principle of all things protocol, so we abstract the network request into the MBRequestable protocol.

First, MBRequestable is an empty protocol.

/// Network request protocol, object conforms to this protocol can make network request
public protocol MBRequestable: class {}Copy the code

Why an empty protocol, because there is no need for objects following the protocol to do anything.

Then extend it to implement a series of interfaces related to network requests:

func request(_ form: MBRequestFormable) -> DataRequest

func download(_ form: MBDownloadFormable) -> DownloadRequest

func download(_ form: MBDownloadResumeFormable) -> DownloadRequest

func upload(_ form: MBUploadDataFormable) -> UploadRequest

func upload(_ form: MBUploadFileFormable) -> UploadRequest

func upload(_ form: MBUploadStreamFormable) -> UploadRequest

func upload(_ form: MBUploadMultiFormDataFormable, completion: ((UploadRequest) -> Void)?Copy the code

These are the interfaces of the network request, and the parameters are various form protocols. The interfaces called inside the interfaces are actually the corresponding interfaces of Alamofire. Note that they all return objects of type DataRequest, UploadRequest, or DownloadRequest, and we can continue to call other methods by returning values.

At this point, the implementation of MBRequestable is complete. Using the method is very simple, just set the type to follow the MBRequestable protocol, and you can make network requests within that type. As follows:

class LoadableViewController: UIViewController.MBRequestable {
    override func viewDidLoad(a) {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        request(WeatherForm()}}Copy the code

loading

The points we care about for loading are as follows:

  • What does it take to start loading?
  • What to do after loading.
  • Whether the loading mask needs to be displayed.
  • Where to display masks.
  • Displays the contents of the mask.

For these points, my classification of agreements is as follows:

  • MBContainableThe agreement. Objects that follow this protocol can be loaded containers.
  • MBMaskableThe agreement. Complying with the agreementUIViewCan be used as a loading mask.
  • MBLoadableThe agreement. Objects that follow this protocol can define the loaded configuration and flow.

MBContainable

Objects that follow this protocol simply implement the following methods:

func containerView(a) -> UIView?
Copy the code

This method returns the UIView as a mask container. The UIView that is the mask will eventually be added to the containerView.

Different types of containers have different containerViews. Here is a list of containerViews:

The container containerView
UIViewController view
UIView self
UITableViewCell contentView
UIScrollView The last one wasn’t.UIScrollViewsuperview

UIScrollView is a little bit special, because if you add a mask view directly to the UIScrollView, the center of the mask view is very difficult to control, so we use a trick, recursively looking for the superView of the UIScrollView, Find that the UIScrollView is not a direct return. The code is as follows:

public override func containerView(a) -> UIView? {
    var next = superview
    while nil! = next {if let _ = next as? UIScrollView{ next = next? .superview }else {
            return next
        }
    }
    return nil
}
Copy the code

Finally, we extended MBContainable to add a latestMask method. This method simply returns the latest MBMaskable subview to the containerView.

MBMaskable

Only one attribute, maskId, is defined inside the protocol to distinguish between masks.

MBNetwork implements two MBActivityIndicator (MBActivityIndicator) and MBMaskView (MBProgressHUD). So for most scenarios, just use these two UIViews.

Note: The MBMaskable protocol is only used to distinguish it from the other containerView subviews.

MBLoadable

As a core part of the loading protocol, MBLoadable contains the following parts:

  • func mask() -> MBMaskable?: Mask view, optional because masks may not be required.
  • func inset() -> UIEdgeInsets: Margin of mask view and container view, default valueUIEdgeInsets.zero.
  • func maskContainer() -> MBContainable?: Mask container view, optional because a mask may not be required.
  • func begin(): loads the start callback method.
  • func end(): Loading end callback method.

Then the protocol requirements of several methods to do the default implementation:

func mask(a) -> MBMaskable? {
    return MBMaskView(a)// Displays the MBProgressHUD effect mask by default.
}

 func inset(a) -> UIEdgeInsets {
    return UIEdgeInsets.zero // Default margin is 0.
}

func maskContainer(a) -> MBContainable? {
    return nil // There is no mask container by default.
}

func begin(a) {
    show() // The show method is called by default.
}

func end(a) {
    hide() // The hide method is called by default.
}
Copy the code

The show and hide methods in the above code are the core code that implements the load mask.

The contents of the show method are as follows:

func show(a) {
    if let mask = self.mask() as? UIView {
        var isHidden = false
        if let _ = self.maskContainer()? .latestMask() { isHidden =true
        }
        self.maskContainer()? .containerView()? .addMBSubView(mask, insets:self.inset())
        mask.isHidden = isHidden

        if let container = self.maskContainer(), let scrollView = container as? UIScrollView {
            scrollView.setContentOffset(scrollView.contentOffset, animated: false)
            scrollView.isScrollEnabled = false}}}Copy the code

This method does several things:

  • judgemaskMethod returns is not followedMBMaskableOf the agreementUIViewBecause if notUIViewCannot be added to anotherUIViewOn.
  • throughMBContainableAgreement on thelatestMaskMethod to get the latest addition and followMBMaskableOf the agreementUIView. If so, hide the newly added mask view and add it tomaskContainercontainerViewOn. The reason why there are multiple masks is that multiple network requests may mask one at the same timemaskContainerIn addition, multiple masks cannot all be shown, because some masks may have translucent parts and therefore need to be hidden. As for why to add tomaskContainerBecause we don’t know which request will end, we take masks for each request we add, then we remove masks for each request we end, and when all requests end, all masks are removed.
  • rightmaskContainerUIScrollViewDo special treatment to make it unscrollable.

Then there is the hide method, which looks like this:

func hide(a) {
    if let latestMask = self.maskContainer()? .latestMask() { latestMask.removeFromSuperview()if let container = self.maskContainer(), let scrollView = container as? UIScrollView {
            if false == latestMask.isHidden {
                scrollView.isScrollEnabled = true}}}}Copy the code

The hide method is simpler than the show method. The latestMask method on the MBContainable protocol is used to fetch the latest UIView that follows the MBMaskable protocol and remove it from the superView. Make it special for maskContainer to be UIScrollView, so that it can be rolled again when the removed mask is the last.

MBLoadType

To reduce usage costs, MBNetwork provides the MBLoadType enumeration type.

public enum MBLoadType {
    case none
    case `default`(container: MBContainable)}Copy the code

None: indicates that no load is required. Default: The added value of the CONTAINER that complies with the MBContainable protocol is passed.

Then extend MBLoadType to follow the MBLoadable protocol.

extension MBLoadType: MBLoadable {
    public func maskContainer(a) -> MBContainable? {
        switch self {
        case .default(let container):
            return container
        case .none:
            return nil}}}Copy the code

This allows MBLoadType to be used directly instead of MBLoadable in cases where no loading is required or where only maskContainer needs to be specified (PS: such as a full-screen mask).

Support for Common Controls

UIControl

  • maskContainerIs itself, for exampleUIButton, directly display “chrysanthemum” on the button when loading.
  • maskIt needs to be customized and cannot be the default valueMBMaskView, but should beMBActivityIndicatorAnd thenMBActivityIndicatorThe “chrysanthemum” color and background color should matchUIControlConsistent.
  • Set when the load starts and when the load endsisEnabled.

UIRefreshControl

  • There is no need to show a loading mask.
  • Called when the load starts and when the load endsbeginRefreshingendRefreshing.

UITableViewCell

  • maskContainerIs itself.
  • maskIt needs to be customized and cannot be the default valueMBMaskView, but should beMBActivityIndicatorAnd thenMBActivityIndicatorThe “chrysanthemum” color and background color should matchUIControlConsistent.

Combine network request

At this point, the definitions and default implementations of the relevant protocols have been loaded. All you need to do now is combine the load with the network request. It’s actually quite simple. The network request methods extended by the MBRequestable protocol all return objects of type DataRequest, UploadRequest, or DownloadRequest. So we can extend them and implement the load method below.

func load(load: MBLoadable = MBLoadType.none) -> Self {
    load.begin()
    return response { (response: DefaultDataResponse) in
        load.end()
    }
}
Copy the code

The parameter passed in is a load object that complies with the MBLoadable protocol. The default value is mbloadType.None. Its begin method is called when the request begins and its end method is called when the request returns.

Method of use

Basic usage

inUIViewControllerThe loading mask is displayed on

request(WeatherForm()).load(load: MBLoadType.default(container: self))
Copy the code
inUIButtonThe loading mask is displayed on

request(WeatherForm()).load(load: button)
Copy the code
inUITableViewCellThe loading mask is displayed on

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView .deselectRow(at: indexPath, animated: false)
    let cell = tableView.cellForRow(at: indexPath)
    request(WeatherForm()).load(load: cell!)
}
Copy the code
UIRefreshControl

refresh.attributedTitle = NSAttributedString(string: "Loadable UIRefreshControl")
refresh.addTarget(self, action: #selector(LoadableTableViewController.refresh(refresh:)), for: .valueChanged)
tableView.addSubview(refresh)
     
func refresh(refresh: UIRefreshControl) {
    request(WeatherForm()).load(load: refresh)
}
Copy the code

The advanced

In addition to the basic usage, MBNetwork also supports full customization of loads as follows:

First, we create a LoadConfig type that follows the MBLoadable protocol.

class LoadConfig: MBLoadable {
    init(container: MBContainable? = nil, mask: MBMaskable? = MBMaskView(), inset: UIEdgeInsets = UIEdgeInsets.zero) {
        insetMine = inset
        maskMine = mask
        containerMine = container
    }
    
    func mask(a) -> MBMaskable? {
        return maskMine
    }
    
    func inset(a) -> UIEdgeInsets {
        return insetMine
    }
    
    func maskContainer(a) -> MBContainable? {
        return containerMine
    }
    
    func begin(a) {
        show()
    }
    
    func end(a) {
        hide()
    }
    
    var insetMine: UIEdgeInsets
    var maskMine: MBMaskable?
    var containerMine: MBContainable?
}
Copy the code

And then we can use it like this.

let load = LoadConfig(container: view, mask:MBEyeLoading(), inset: UIEdgeInsetsMake(30+64.15.UIScreen.main.bounds.height-64- (44*4+30+15*3), 15))
request(WeatherForm()).load(load: load)
Copy the code

You’ll find that everything is customizable and still simple to use.

Here is an example of using LoadConfig to display a custom load mask on a UITableView.


let load = LoadConfig(container:self.tableView, mask: MBActivityIndicator(), inset: UIEdgeInsetsMake(UIScreen.main.bounds.width - self.tableView.contentOffset.y > 0 ? UIScreen.main.bounds.width - self.tableView.contentOffset.y : 0, 0, 0, 0))
request(WeatherForm()).load(load: load)
        
Copy the code

Loading Progress Display

Showing progress is relatively easy. You just need a way to update progress in real time, so we define the MBProgressable protocol, which looks like this:

public protocol MBProgressable {
    func progress(_ progress: Progress)
}
Copy the code

Since progress display is generally required only for uploading and downloading large files, we only make extension for UploadRequest and DownloadRequest and add the progress method. The parameter is a progress object that follows the MBProgressable protocol:

func progress(progress: MBProgressable) -> Self {
    return uploadProgress { (prog: Progress) in
        progress.progress(prog)
    }
}

Copy the code

Support for Common Controls

UIProgressView is bound by the MBProgressable protocol.

// MARK: - Making `UIProgressView` conforms to `MBLoadProgressable`
extension UIProgressView: MBProgressable {

    /// Updating progress
    ///
    /// - Parameter progress: Progress object generated by network request
    public func progress(_ progress: Progress) {
        self.setProgress(Float(progress.completedUnitCount).divided(by: Float(progress.totalUnitCount)), animated: true)}}Copy the code

Then we can take the UIProgressView object directly as an argument to the progress method.

download(ImageDownloadForm()).progress(progress: progress)
Copy the code

message

Information prompt includes two parts, error prompt and success prompt. Therefore, we first abstract an MBMessageable protocol, the content of which only contains the container to display the message.

public protocol MBMessageable {
    func messageContainer(a) -> MBContainable?
}
Copy the code

Of course, the returned container is also compliant with the MBContainable protocol and will be used to display error and success messages.

Error messages

There are two steps to doing the error prompt:

  1. Parsing error messages
  2. Display error messages

First, let’s complete the first step, parsing the error message. Here we abstract the error message into the protocol MBErrorable, which reads as follows:

public protocol MBErrorable {

    /// Using this set with code to distinguish successful code from error code
    var successCodes: [String] { get }

    /// Using this code with successCodes set to distinguish successful code from error code
    var code: String? { get }

    /// Corresponding message
    var message: String? { get}}Copy the code

SuccessCodes define which error codes are normal; Code indicates the current error code. Message defines the information to be presented to the user.

How to use this protocol will be discussed later, but we will continue to look at the JSON error resolution protocol MBJSONErrorable.

public protocol MBJSONErrorable: MBErrorable.Mappable {}Copy the code

Note that the Mappable protocol is derived from ObjectMapper. The purpose is to implement func mapping(map: Map method, which defines the mapping of error messages in JSON data to code and message properties in the MBErrorable protocol.

Suppose the server returns the following JSON:

{
    "data": {
        "code": "200"."message": "Request successful"}}Copy the code

Then our error message object can be defined as follows.

class WeatherError: MBJSONErrorable {
    var successCodes: [String] = ["200"]

    var code: String?
    var message: String?

    init() {}required init? (map: Map) {}func mapping(map: Map) {
        code <- map["data.code"]
        message <- map["data.message"]}}Copy the code

ObjectMapper maps the values of data.code and data.message to the code and Message properties. At this point, parsing of the error message is complete.

Then comes the second step, error message display. Define the MBWarnable protocol:

public protocol MBWarnable: MBMessageable {
    func show(error: MBErrorable?)
}
Copy the code

This agreement follows the MBMessageable agreement. In addition to implementing the messageContainer method of MBMessageable, an object that follows this protocol also implements the show method, which has only one argument, through which we pass the object that follows the error message protocol.

Now we can use the MBErrorable and MBWarnable protocols for error notification. As before, we’ll do extension to the DataRequest. Add the WARN method.

func warn<T: MBJSONErrorable>(
        error: T,
        warn: MBWarnable,
        completionHandler: ((MBJSONErrorable) -> Void)? = nil) - >Self {

    return response(completionHandler: { (response: DefaultDataResponse) in
        if let err = response.error {
            warn.show(error: err.localizedDescription)
        }
    }).responseObject(queue: nil, keyPath: nil, mapToObject: nil, context: nil) { (response: DataResponse<T>) in
        if let err = response.result.value {
            if let code = err.code {
                if true == error.successCodes.contains(code) { completionHandler? (err) }else {
                    warn.show(error: err)
                }
            }
        }
    }
}
Copy the code

This method takes three arguments:

  • error: followMBJSONErrorableThe generic error resolution object for the protocol. Pass this object toAlamofireObjectMapperresponseObjectMethod to get the error message returned by the server.
  • warn: followMBWarnableError display object for protocol.
  • completionHandler: closure to call when the result is correct. The business layer typically uses this closure for special error code handling.

Did the following:

  • Alamofire’s response method gets a non-business error message. If there is one, warn’s show method is called to display the error message. That’s because we did the following:

    extension String: MBErrorable {
          public var message: String? {
              return self}}Copy the code
  • The responseObject method of AlamofireObjectMapper is used to obtain the error message returned by the server and determine whether the returned error code is included in the successCodes. If so, it is handed over to the business layer for processing. (PS: Error codes that require special handling can also be defined in successCodes and handled separately in the business layer.) Otherwise, call warn’s show method directly to display the error message.

Successful tip

Compared with the error message, the success message is simpler because the success message is defined locally and does not need to be obtained from the server. Therefore, the content of the success message protocol is as follows:

public protocol MBInformable: MBMessageable {
    func show(a)

    func message(a) -> String
}
Copy the code

The show method is used to display information. The message method defines the information to display.

Then extend DataRequest and add inform method:

func inform<T: MBJSONErrorable>(error: T, inform: MBInformable) -> Self {

    return responseObject(queue: nil, keyPath: nil, mapToObject: nil, context: nil) { (response: DataResponse<T>) in
        if let err = response.result.value {
            if let code = err.code {
                if true == error.successCodes.contains(code) {
                    inform.show()
                }
            }
        }
    }
}
Copy the code

Generic error resolution objects that follow the MBJSONErrorable protocol are also passed in here, because if the server returns an error, success should not be prompted. Or use the responseObject method of AlamofireObjectMapper to get the error message returned by the server and determine whether the returned error code is included in the successCodes. If so, Run the show method on the Inform object to display the success information.

Support for Common Controls

Observing the current mainstream App, the message prompt is generally displayed through UIAlertController, so we use extension to make UIAlertController follow MBWarnable and MBInformable protocols.

extension UIAlertController: MBInformable {
    public func show(a) {
        UIApplication.shared.keyWindow? .rootViewController? .present(self, animated: true, completion: nil)}}extension UIAlertController: MBWarnable{
    public func show(error: MBErrorable?) {
        if let err = error {
            if ""! = err.message { message = err.messageUIApplication.shared.keyWindow? .rootViewController? .present(self, animated: true, completion: nil)}}}}Copy the code

Found we didn’t use messageContainer here, this is because for UIAlertController, its container is fixed, use the UIApplication. Shared. KeyWindow? .rootViewController? Can. Note that for MBInformable, UIAlertController is displayed directly, and for MBWarnable, message in Error is displayed.

Here are two examples in use:

let alert = UIAlertController(title: "Warning", message: "Network unavailable", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.cancel, handler: nil))
        
request(WeatherForm()).warn(
    error: WeatherError(),
    warn: alert
)

let alert = UIAlertController(title: "Notice", message: "Load successfully", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.cancel, handler: nil))
request(WeatherForm()).inform(
    error: WeatherInformError(),
    inform: alert
)
Copy the code

In this way, the business layer defines the display information, and MBNetwork automatically displays the effect, isn’t it much easier? As far as extensibility is concerned, we can also add support for other third-party hint libraries, similar to UIAlertController’s implementation.

To request

In the development… Stay tuned for

Please correct any intellectual property rights, copyright issues or theoretical errors.

Please indicate the original author and above information.