ios Uso adequado do URLRequestConvertible da Alamofire



swift (4)

Ótimas perguntas. Vamos dividir cada um individualmente.

Qual é o uso adequado de URLRequestConvertible na API do mundo real?

O protocolo URLRequestConvertible é uma maneira leve de garantir que um determinado objeto possa criar um NSURLRequest válido. Não existe realmente um conjunto estrito de regras ou diretrizes que o forçam a usar esse protocolo de qualquer maneira específica. É apenas um protocolo de conveniência para permitir que outros objetos armazenem o estado necessário para criar corretamente o NSURLRequest . Mais algumas informações relacionadas à Alamofire podem ser encontradas here .

Devo criar um roteador por ponto de extremidade?

Definitivamente não. Isso anularia todo o propósito de usar um Enum . Os objetos Swift Enum são incrivelmente poderosos, permitindo que você compartilhe uma grande quantidade de estado comum e ligue as partes que são realmente diferentes. Ser capaz de criar um NSURLRequest com algo tão simples quanto o seguinte é realmente poderoso!

let URLRequest: NSURLRequest = Router.ReadUser("cnoon")

Não consigo descobrir por que o enum é usado para construir roteadores? Por que não usamos classe com métodos estáticos?

Um enum está sendo usado porque é uma maneira muito mais concisa de expressar vários objetos relacionados em uma interface comum. Todos os métodos são compartilhados entre todos os casos. Se você usou métodos estáticos, teria que ter um método estático para cada caso para cada método. Ou você teria que usar uma enum com estilo Obj-C dentro do objeto. Aqui está um exemplo rápido do que quero dizer.

enum Router: URLRequestConvertible {
    static let baseURLString = "http://example.com"

    case CreateUser([String: AnyObject])
    case ReadUser(String)
    case UpdateUser(String, [String: AnyObject])
    case DestroyUser(String)

    var method: Alamofire.HTTPMethod {
        switch self {
        case .CreateUser:
            return .post
        case .ReadUser:
            return .get
        case .UpdateUser:
            return .put
        case .DestroyUser:
            return .delete
        }
    }

    var path: String {
        switch self {
        case .CreateUser:
            return "/users"
        case .ReadUser(let username):
            return "/users/\(username)"
        case .UpdateUser(let username, _):
            return "/users/\(username)"
        case .DestroyUser(let username):
            return "/users/\(username)"
        }
    }
}

Para obter o método de qualquer um dos diferentes pontos de extremidade, você pode chamar o mesmo método sem precisar passar nenhum parâmetro para definir que tipo de ponto de extremidade você está procurando, já é tratado pelo caso selecionado.

let createUserMethod = Router.CreateUser.method
let updateUserMethod = Router.UpdateUser.method

Ou, se você deseja obter o caminho, os mesmos tipos de chamadas.

let updateUserPath = Router.UpdateUser.path
let destroyUserPath = Router.DestroyUser.path

Agora vamos tentar a mesma abordagem usando métodos estáticos.

struct Router: URLRequestConvertible {
    static let baseURLString = "http://example.com"

    static var method: Method {
        // how do I pick which endpoint?
    }

    static func methodForEndpoint(endpoint: String) -> Method {
        // but then I have to pass in the endpoint each time
        // what if I use the wrong key?
        // possible solution...use an Obj-C style enum without functions?
        // best solution, merge both concepts and bingo, Swift enums emerge
    }

    static var path: String {
        // bummer...I have the same problem in this method too.
    }

    static func pathForEndpoint(endpoint: String) -> String {
        // I guess I could pass the endpoint key again?
    }

    static var pathForCreateUser: String {
        // I've got it, let's just create individual properties for each type
        return "/create/user/path"
    }

    static var pathForUpdateUser: String {
        // this is going to get really repetitive for each case for each method
        return "/update/user/path"
    }

    // This approach gets sloppy pretty quickly
}

NOTA: Se você não possui muitas propriedades ou funções que ativam os casos, uma enumeração não apresenta muitas vantagens sobre uma estrutura. É simplesmente uma abordagem alternativa com diferentes açúcares sintáticos.

As enums podem maximizar a reutilização de estado e código. Os valores associados também permitem que você faça coisas realmente poderosas, como agrupar objetos que são um pouco semelhantes, mas têm requisitos incrivelmente diferentes ... como a criação de NSURLRequest .

Qual é a maneira correta de construir parâmetros para casos enum para melhorar a legibilidade? (teve que misturar este aqui)

Essa é uma pergunta fantástica. Você já definiu duas opções possíveis. Deixe-me acrescentar um terceiro que possa atender às suas necessidades um pouco melhor.

case CreateUser(username: String, firstName: String, lastName: String, email: String)
case ReadUser(username: String)
case UpdateUser(username: String, firstName: String, lastName: String, email: String)
case DestroyUser(username: String)

Nos casos em que você tem valores associados, acho que pode ser útil adicionar nomes explícitos para todos os valores na tupla. Isso realmente ajuda a construir o contexto. A desvantagem é que você precisará redeclarar esses valores em suas instruções de switch dessa maneira.

static var method: String {
    switch self {
    case let CreateUser(username: username, firstName: firstName, lastName: lastName, email: email):
        return "POST"
    default:
        return "GET"
    }
}

Embora isso proporcione um contexto agradável e consistente, ele fica bem detalhado. Essas são suas três opções no momento no Swift, qual é a correta para usar, dependendo do seu caso de uso.

Atualizar

Com o lançamento do Alamofire 4.0, o URLRequestConvertible agora pode ser MUITO mais inteligente e também pode ser lançado. Adicionamos suporte completo à Alamofire para lidar com solicitações inválidas e gerar erros sensíveis por meio dos manipuladores de resposta. Este novo sistema está documentado em detalhes em nosso README .

https://src-bin.com

Eu li alguns tutoriais, o README do @mattt, mas não consigo entender algumas coisas.

  1. Qual é o uso adequado de URLRequestConvertible na API do mundo real? Parece que vou criar um roteador implementando o protocolo URLRequestConvertible para toda a API - dificilmente será legível. Devo criar um roteador por ponto de extremidade?

  2. Segunda pergunta provavelmente causada por falta de experiência com a linguagem Swift. Não consigo descobrir por que o enum é usado para construir roteadores? Por que não usamos classe com métodos estáticos? Aqui está um exemplo (do README da Alamofire)

    enum Router: URLRequestConvertible {
        static let baseURLString = "http://example.com"
        static let perPage = 50
    
        case Search(query: String, page: Int)
    
        // MARK: URLRequestConvertible
    
        var URLRequest: NSURLRequest {
            let (path: String, parameters: [String: AnyObject]?) = {
                switch self {
                case .Search(let query, let page) where page > 1:
                    return ("/search", ["q": query, "offset": Router.perPage * page])
                case .Search(let query, _):
                    return ("/search", ["q": query])
                }
            }()
    
            let URL = NSURL(string: Router.baseURLString)!
            let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(path))
            let encoding = Alamofire.ParameterEncoding.URL
    
            return encoding.encode(URLRequest, parameters: parameters).0
        }
    }
  3. Existem 2 maneiras de passar parâmetros:

    case CreateUser([String: AnyObject])
    case ReadUser(String)
    case UpdateUser(String, [String: AnyObject])
    case DestroyUser(String)

    e (digamos que o usuário tenha 4 parâmetros)

    case CreateUser(String, String, String, String)
    case ReadUser(String)
    case UpdateUser(String, String, String, String, String)
    case DestroyUser(String)

    @mattt está usando o primeiro no exemplo. Mas isso levará a nomes de parâmetros "codificados" fora do roteador (por exemplo, em UIViewControllers). Erro de digitação no nome do parâmetro pode levar a erro.
    Outras pessoas estão usando a segunda opção, mas nesse caso não é óbvio o que cada parâmetro representa.
    Qual será o caminho certo para fazer isso?


Answer #1

Aqui está o enum Router atualizado no Swift 3, recomendado no here . Espero que você ache útil em termos de como implementar corretamente um roteador com URLRequestConvertible .

import Alamofire

enum Router: URLRequestConvertible
{
    case createUser(parameters: Parameters)
    case readUser(username: String)
    case updateUser(username: String, parameters: Parameters)
    case destroyUser(username: String)

    static let baseURLString = "https://example.com"

    var method: HTTPMethod
    {
        switch self {
        case .createUser:
            return .post
        case .readUser:
            return .get
        case .updateUser:
            return .put
        case .destroyUser:
            return .delete
        }
     }

    var path: String
    {
        switch self {
        case .createUser:
            return "/users"
        case .readUser(let username):
            return "/users/\(username)"
        case .updateUser(let username, _):
            return "/users/\(username)"
        case .destroyUser(let username):
            return "/users/\(username)"
        }
    }

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest
    {
        let url = try Router.baseURLString.asURL()

        var urlRequest = URLRequest(url: url.appendingPathComponent(path))
        urlRequest.httpMethod = method.rawValue

        switch self {
        case .createUser(let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        case .updateUser(_, let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        default:
            break
        }

        return urlRequest
    }
}

Answer #2

Os tipos que adotam o protocolo URLRequestConvertible podem ser usados ​​para construir solicitações de URL.

Aqui está um exemplo retirado de www.raywenderlich.com

public enum ImaggaRouter : URLRequestConvertible{

  static let baseURL = "http://api.imagga.com/v1"
  static let authenticationToken = "XAFDSADGDFSG DAFGDSFGL"

  case Content, Tags(String), Colors(String)

  public var URLRequest: NSMutableURLRequest {
    let result: (path: String, method: Alamofire.Method, parameters: [String: AnyObject]) = {
      switch self {
      case .Content:
        return ("/content", .POST, [String: AnyObject]())
      case .Tags(let contentID):
        let params = [ "content" : contentID ]
        return ("/tagging", .GET, params)
      case .Colors(let contentID):
        let params = [ "content" : contentID, "extract_object_colors" : NSNumber(int: 0) ]
        return ("/colors", .GET, params)
      }
    }()

    let URL = NSURL(string: ImaggaRouter.baseURL)!
    let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
    URLRequest.HTTPMethod = result.method.rawValue
    URLRequest.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization")
    URLRequest.timeoutInterval = NSTimeInterval(10 * 1000)

    let encoding = Alamofire.ParameterEncoding.URL
    return encoding.encode(URLRequest, parameters: result.parameters).0
  }
}

e podemos usar esse ImmageRouter da seguinte maneira:

Alamofire.request(ImaggaRouter.Tags(contentID))
      .responseJSON{ response in

Answer #3

Por que você não tenta usar o SweetRouter . Isso ajudará você a remover todo o padrão que você possui ao declarar um roteador e também suporta coisas como vários ambientes, e seu código será realmente legível.

Aqui está um exemplo do roteador com roteador doce:

struct Api: EndpointType {
    enum Environment: EnvironmentType {
        case localhost
        case test
        case production

        var value: URL.Environment {
            switch self {
            case .localhost: return .localhost(8080)
            case .test: return .init(IP(126, 251, 20, 32))
            case .production: return .init(.https, "myproductionserver.com", 3000)
            }
        }
    }

    enum Route: RouteType {
        case auth, me
        case posts(for: Date)

        var route: URL.Route {
            switch self {
            case .me: return .init(at: "me")
            case .auth: return .init(at: "auth")
            case let .posts(for: date):
                return URL.Route(at: "posts").query(("date", date), ("userId", "someId"))
            }
        }
    }

    static let current: Environment = .localhost
}

E aqui está como você o usaria:

Alamofire.request(Router<Api>(at: .me))
Alamofire.request(Router<Api>(.test, at: .auth))
Alamofire.request(Router<Api>(.production, at: .posts(for: Date())))




alamofire