When working with Moya, you encounter situations where parameters need to be set dynamically in the URL, and parameters need to be set in the body.

For example, you need to add a userID to the requested URL and pass a JSON-formatted parameter {“roomID”: “123”} to the body.

Both the URL and the body are submitted

Option 1 (Failed)

Concatenate url parameters in the path method and submit body parameters in the task.

public enum LiveShowRequest {
    case anchorHeartBeat(userID: String,
                         roomID: String)}extension LiveShowRequest: TargetType {
    
    public var path: String {
        switch self {
        case .anchorHeartBeat(let userID, _) :return "anchor_heartbeat? userID=\(userID)"}}public var task: Task {
        switch self {
        case let .anchorHeartBeat(_, roomID):

            var params = ["roomID": roomID] as [String : Any]
            return .requestParameters(parameters: params, encoding: JSONEncoding.default)
        }
    }
}
Copy the code

After testing, this does not work because Moya encodes the return value of path, resulting in? Is encoded as %3f, the server can’t parse the parameter.

Plan 2 (success)

In the task using requestCompositeParameters, bodyParameters and urlParameters can be set at the same time.

extension LiveShowRequest: TargetType {
    
    public var path: String {
        switch self {
        case .anchorHeartBeat(let userID, _) :return "anchor_heartbeat"}}public var task: Task {
        switch self {
        case let .anchorHeartBeat(userID, roomID):
            var params = ["roomID": roomID] as [String : Any]
            var urlParameters = ["userID": userID]
            
            return .requestCompositeParameters(bodyParameters: params,
                                               bodyEncoding: JSONEncoding.default,
                                               urlParameters: urlParameters)
        }
    }
    
}
Copy the code

This can handle the case where both the URL and the body arguments are passed in.

Method 3 (Success)

Concatenate url parameters in baseURL and set body parameters in task to avoid method 1? The problem of being coded.

extension LiveShowRequest: TargetType {
    
    public var baseURL: URL {
        switch self {
        case let .anchorHeartBeat(userID, _) :return URL(string: BaseURL + "? userID=\(userID)")!}}public var path: String {
        switch self {
        case .anchorHeartBeat:
            return "anchor_heartbeat"}}public var task: Task {
        switch self {
        case let .anchorHeartBeat(_, roomID):
            var params = ["roomID": roomID] as [String : Any]
            return .requestParameters(parameters: params, encoding: JSONEncoding.default)
        }
    }
Copy the code

Url parameter encoding problem

If the url argument passed in contains *, such as userID 0558eba*1400489990 in the example above, Moya will convert * to %2A, which is the URL encoding. But the * itself does not require URL encoding, so why does Moay encode the *?

In Moya, url parameters are encoded through URLEncoding.

public struct URLEncoding: ParameterEncoding {
    
    /// Returns a percent-escaped string following RFC 3986 for a query string key or value.
    ///
    /// RFC 3986 states that the following characters are "reserved" characters.
    ///
    /// - General Delimiters: ":", "#", "[", "]", "@", "?" , "/"
    /// - Sub-Delimiters: "!" "," $", "&", "'", "*", "+", ",", "; , "="
    ///
    // In RFC 3986 - Section 3.4, it states that the "? and "/" characters should not be escaped to allow
    /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
    /// should be percent-escaped in the query string.
    ///
    /// - parameter string: The string to be percent-escaped.
    ///
    /// - returns: The percent-escaped string.
    public func escape(_ string: String) -> String {
        let generalDelimitersToEncode = : # @ "[]" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = ! "" * + $& '(),; ="

        var allowedCharacterSet = CharacterSet.urlQueryAllowed
        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")

        var escaped = ""

        / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
        //
        Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
        // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
        // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
        // info, please refer to:
        //
        // - https://github.com/Alamofire/Alamofire/issues/206
        //
        / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

        if #available(iOS 8.3.*) {
            escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
        } else {
            let batchSize = 50
            var index = string.startIndex

            while index ! = string.endIndex {
                let startIndex = index
                let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
                let range = startIndex..<endIndex

                let substring = string[range]

                escaped + = substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? String(substring)

                index = endIndex
            }
        }

        return escaped
    }
}
Copy the code

The escape method is used to encode the key and value of parameters concatenated into the URL. The escape method returns a percentage sign escape string according to RFC 3986. Before calling addingPercentEncoding, First removed the CharacterSet. UrlQueryAllowed contained in generalDelimitersToEncode and subDelimitersToEncode characters.

let generalDelimitersToEncode = : # @ "[]" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = ! "" * + $& '(),; ="

var allowedCharacterSet = CharacterSet.urlQueryAllowed
allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
Copy the code

Can be extended by the following method to check the included in the CharacterSet. UrlQueryAllowed string. (Method from StackOverflow)

extension CharacterSet {
    func characters(a)- > [Character] {
        // A Unicode scalar is any Unicode code point in the range U+0000 to U+D7FF inclusive or U+E000 to U+10FFFF inclusive.
        return codePoints().compactMap { UnicodeScalar($0) }.map { Character($0)}}func codePoints(a)- > [Int] {
        var result: [Int] = []
        var plane = 0
        // following documentation at https://developer.apple.com/documentation/foundation/nscharacterset/1417719-bitmaprepresentation
        for (i, w) in bitmapRepresentation.enumerated() {
            let k = i % 0x2001
            if k = = 0x2000 {
                // plane index byte
                plane = Int(w) << 13
                continue
            }
            let base = (plane + k) << 3
            for j in 0 ..< 8 where w & 1 << j ! = 0 {
                result.append(base + j)
            }
        }
        return result
    }
}

// How to use it
var allowedCharacterSet = CharacterSet.urlQueryAllowed
debugPrint(allowedCharacterSet.characters())
/ / the result
["!"."$"."&"."\ '"."(".")"."*"."+".","."-"."."."/"."0"."1"."2"."3"."4"."5"."6"."Seven"."8"."9".":".";"."="."?"."@"."A"."B"."C"."D"."E"."F"."G"."H"."I"."J"."K"."L"."M"."N"."O"."P"."Q"."R"."S"."T"."U"."V"."W"."X"."Y"."Z"."_"."a"."b"."c"."d"."e"."f"."g"."h"."i"."j"."k"."l"."m"."n"."o"."p"."q"."r"."s"."t"."u"."v"."w"."x"."y"."z"."~"]
Copy the code

What if you happen to have these characters in the URL argument, and you don’t want to encode them? Only one type that conforms to ParameterEncoding can be defined to implement custom encoding.

The following MultipleEncoding is provided by github.com/Moya/Moya/i… Provides the idea of implementation. When MultipleEncoding is initialized, the urlParameters define the key to be urL-encoded, and the rest of the key is encoded as body.

struct MultipleEncoding : ParameterEncoding {

    var urlParameters: [String]?
    
    init(urlParameters: [String]?) {
        self.urlParameters = urlParameters
    }
    
    func encode(_ urlRequest: Alamofire.URLRequestConvertible.with parameters: Parameters?). throws -> URLRequest {
        guard let parameters = parameters else { return urlRequest as! URLRequest }
        
        
        var urlParams: [String: Any] = [:]
        var jsonParams: [String: Any] = [:]
        
        parameters.forEach { (key: String, value: Any) in
            if urlParameters?.contains(key) ?? false {
                urlParams[key] = value
            } else {
                jsonParams[key] = value
            }
        }
        
        // Encode URL Params
		/ / process and URLEncoding. The queryString. Encode (urlRequest, with: urlParams)
        var urlRequest = try urlRequest.asURLRequest()
        if let url = urlRequest.url {
            if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !urlParams.isEmpty {
                let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(urlParams)
                urlComponents.percentEncodedQuery = percentEncodedQuery
                urlRequest.url = urlComponents.url
            }
        }
        
        //Encode JSON
        return try JSONEncoding.default.encode(urlRequest, with: jsonParams)
    }
    
    private func query(_ parameters: [String: Any]) -> String {
        // Copy the method in URLEncoding directly
    }
    
    public func queryComponents(fromKey key: String.value: Any)- > [(String.String)] {
        // Copy the method in URLEncoding directly
    }
    
    // escape only removes unencoded characters as needed
    public func escape(_ string: String) -> String {
        let generalDelimitersToEncode = : # @ "[]" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = ! "" * + $& '(),; ="

        var allowedCharacterSet = CharacterSet.urlQueryAllowed
// allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")

        // The following code is consistent with the URLEncoding method}}Copy the code

Use as follows.

extension LiveShowRequest: TargetType {
    
    public var path: String {
        switch self {
        case .anchorHeartBeat(let userID, _) :return "anchor_heartbeat"}}public var task: Task {
        switch self {
        case let .anchorHeartBeat(userID, roomID):
            var params = ["roomID": roomID,
                          "userID": userID,
                          ] as [String : Any]
            
            let urlParameters = ["userID"]
            return .requestParameters(parameters: params, encoding: MultipleEncoding(urlParameters: urlParameters))
        }
    }
}
Copy the code

This customizes the entire process of URL and body coding.

reference

Github.com/Moya/Moya/i…