Uso adecuado de URLRequestConvertible de Alamofire

79

He leído un par de tutoriales, README de @mattt pero no puedo entender un par de cosas.

  1. ¿Cuál es el uso adecuado de URLRequestConvertiblela API del mundo real? Parece que si crearé un enrutador implementando el URLRequestConvertibleprotocolo para todas las API, será apenas legible. ¿Debo crear un enrutador por punto final?

  2. La segunda pregunta probablemente se debe a la falta de experiencia con el lenguaje Swift. No puedo entender por qué enumse usa para construir enrutadores. ¿Por qué no usamos clases con métodos estáticos? aquí hay un ejemplo (del README de 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. Hay 2 formas de pasar parámetros:

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

    y (digamos que el usuario tiene 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 el primero en el ejemplo. Pero eso conducirá a "codificar" los nombres de los parámetros fuera del enrutador (por ejemplo, en UIViewControllers). Un error tipográfico en el nombre del parámetro puede provocar errores.
    Otras personas están usando la segunda opción, pero en ese caso no es tan obvio lo que representa cada parámetro.
    ¿Cuál será la forma correcta de hacerlo?

OgreSwamp
fuente

Respuestas:

110

Grandes preguntas. Analicemos cada uno individualmente.

¿Cuál es el uso adecuado de URLRequestConvertible en la API del mundo real?

El URLRequestConvertibleprotocolo es una forma ligera de garantizar que un objeto dado pueda crear un archivo NSURLRequest. Realmente no existe un conjunto estricto de reglas o pautas que lo obliguen a usar este protocolo de una manera particular. Es simplemente un protocolo de conveniencia para permitir que otros objetos almacenen el estado requerido para crear correctamente el NSURLRequest. Puede encontrar más información relacionada con Alamofire aquí .

¿Debo crear un enrutador por punto final?

Definitivamente no. Eso frustraría todo el propósito de usar un Enum. Los objetos Swift Enum son increíblemente poderosos y te permiten compartir una gran cantidad de estados comunes y activar las partes que realmente son diferentes. ¡Poder crear un NSURLRequestcon algo tan simple como lo siguiente es realmente poderoso!

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

No puedo entender por qué se usa enum para construir enrutadores. ¿Por qué no usamos clases con métodos estáticos?

Se utiliza una enumeración porque es una forma mucho más concisa de expresar varios objetos relacionados en una interfaz común. Todos los métodos se comparten entre todos los casos. Si usó métodos estáticos, tendría que tener un método estático para cada caso para cada método. O tendría que usar una enumeración de estilo Obj-C dentro del objeto. He aquí un ejemplo rápido de lo que quiero decir.

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 obtener el método de cualquiera de los diferentes puntos finales, puede llamar al mismo método sin tener que pasar ningún parámetro para definir qué tipo de punto final está buscando, ya lo maneja el caso que seleccione.

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

O si desea obtener la ruta, los mismos tipos de llamadas.

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

Ahora intentemos el mismo enfoque 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: Si no tiene muchas propiedades o funciones que activen los casos, entonces una enumeración no presenta muchas ventajas sobre una estructura. Es simplemente un enfoque alternativo con diferente azúcar sintáctico.

Las enumeraciones pueden maximizar la reutilización del código y el estado. Los valores asociados también le permiten hacer cosas realmente poderosas como agrupar objetos que son algo similares, pero tienen requisitos increíblemente diferentes ... como la NSURLRequestcreación.

¿Cuál es la forma correcta de construir parámetros para casos de enumeración para mejorar la legibilidad? (tuve que triturar este juntos)

Esa es una excelente pregunta. Ya ha presentado dos opciones posibles. Permítanme agregar un tercio que puede satisfacer sus necesidades un poco mejor.

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)

En los casos en los que tenga valores asociados, creo que puede ser útil agregar nombres explícitos para todos los valores en la tupla. Esto realmente ayuda a construir el contexto. La desventaja es que luego debe volver a declarar esos valores en sus declaraciones de cambio de esa manera.

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

Si bien esto le brinda un contexto agradable y consistente, se vuelve bastante detallado. Esas son sus tres opciones en este momento en Swift, cuál es la correcta para usar depende de su caso de uso.


Actualizar

Con el lanzamiento de 🔥🔥 Alamofire 4.0 🔥🔥, URLRequestConvertibleahora puede ser MUCHO más inteligente y también puede lanzar. Hemos agregado soporte completo en Alamofire para manejar solicitudes no válidas y generar errores sensibles a través de los controladores de respuesta. Este nuevo sistema está documentado en detalle en nuestro README .

por la tarde
fuente
8
Gracias. Solo una pregunta con respecto a su respuesta sobre un enrutador frente a un enrutador de construcción por punto final (por ejemplo, ejemplo de CRUD de la página de Alamofire). ¿No crees que si tengo, digamos, 5 puntos finales, cada uno tiene 3-4 métodos, son 15-20 casedeclaraciones. Eso me parece un método enorme. No estoy seguro de si eso llevará al código legible ...
OgreSwamp
1
Con respecto a la segunda respuesta (métodos enum vs static), el punto clave para mí aquí es ocultar una implementación dentro de enum / class. No necesito conocer métodos o caminos fuera de él. Quiero llamar Router.createUser("[email protected]", "....")y tener un bloqueo para interpretar Resultados para el servidor. Todos los detalles (métodos, rutas, raíz de la API, etc.) pueden ser privados para el enrutador, eso está bien.
OgreSwamp
2
Para su último comentario, no creo que desee incluir 20 puntos finales diferentes en una sola enumeración si también tuviera un montón de funciones. Sus declaraciones de cambio serían tan largas que no serían muy legibles. Definitivamente olor a código en ese punto. Para mí, una vez que obtienes más de 5 o 6 casos en tus interruptores, realmente comienzas a perder legibilidad.
2015
2
Para su último comentario @cnoon, (leí los comentarios anteriores) está diciendo que (usando su ejemplo de enrutador de usuario CRUD), si tengo algunas solicitudes que pertenecen a diferentes contextos, como Solicitar publicaciones de Twitter y Usuario CRUD, esas ¿Serán dos enrutadores separados?
Renan Kosicki
3
Sí, eso es correcto @RenanKosicki. Ciertamente, llega a un punto sin retorno cuando tiene demasiados casos en una enumeración de enrutador. Dividirlos en grupos lógicos es ciertamente un diseño más deseable.
tarde
8

Aquí está la actualización enum Routerde Swift 3, que se recomienda en Github de Alamofire . Espero que lo encuentre útil en términos de cómo implementar correctamente un enrutador con 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
    }
}
arauter
fuente
7

¿Por qué no intentas usar SweetRouter ? Le ayudará a eliminar todo el texto estándar que tiene al declarar un enrutador y también admite cosas como múltiples entornos y su código será realmente legible.

Aquí hay un ejemplo del enrutador con enrutador dulce:

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
}

Y así es como lo usaría:

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

Encontré una manera de trabajar con él, creé una clase con el enrutador: heredar clases de una solicitud

file request.swift

class request{

    func login(user: String, password: String){
        /*use Router.login(params)*/
    }
    /*...*/
    enum Router: URLRequestConvertible {
        static let baseURLString = "http://example.com"
        static let OAuthToken: String?

        case Login([String: AnyObject])
        /*...*/

        var method: Alamofire.Method {
            switch self {
            case .Login:
                return .POST
            /*...*/
        }

        var path: String {
            switch self {
            case .Login:
                return "/login"
            /*...*/
            }
        }

        var URLRequest: NSURLRequest {
            switch self {
                case .Login(let parameters):
                    return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
                /*...*/
                default:
                    return mutableURLRequest
            }
        }
    }
}

archivo requestContacts.swift

class requestContacts: api{

    func getUser(id: String){
        /*use Router.getUser(id)*/
    }
    /*...*/

    enum Router: URLRequestConvertible {

        case getUser(id: String)
        case setUser([String: AnyObject])

        var method: Alamofire.Method {
            switch self {
                case .getUser:
                    return .GET
                case .setUser:
                    return .POST
                /*...*/
            }
        }

        var path: String {
            switch self {
            case .getUser(id: String):
                return "/user\(id)/"
            case .setUser(id: String):
                return "/user/"
            /*...*/
            }
        }
        // MARK: URLRequestConvertible

        var URLRequest: NSURLRequest {
            //use same baseURLString seted before
            let URL = NSURL(string: Router.baseURLString)!
                let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
                mutableURLRequest.HTTPMethod = method.rawValue

            if let token = Router.OAuthToken {
                mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            }
            switch self {
                /*...*/
                case .setUser(let parameters):
                    return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
                default: //for GET methods, that doesent need more
                    return mutableURLRequest
            }
        }
    }
}

por lo que la clase del hijo obtendrá los parámetros de Router del padre, e incluso puede usar Route.login en cualquier hijo. aún así, no sé si hay una manera de obtener una URLRequest corta, por lo que no necesito establecer parámetros una y otra vez

David Alejandro Londoño Mejía
fuente
Hola, estoy tratando de hacer lo que dijo en su respuesta, pero cuando trato de usar el método POST todavía obtengo la respuesta del método GET. Por ejemplo: cuando accedo a mi url "/ users" en lugar de crear el usuario mediante el método POST, obtengo la lista de todos los usuarios, que es la respuesta del método GET. ¿Alguna idea de por qué está pasando esto? Parece incluso configurar el método mutableURLRequest.HTTPMethod = method.rawValuesin cambios.
Renato Parreira
¿A qué enumeración accediste? tienes que seleccionar la enumeración para una POST, y establecer una POST para ese valor de enumeración, aquí la publicación es Router.setUser (...)
David Alejandro Londoño Mejía
¿Puedes consultar mi pregunta aquí en SO? Allí proporciono todos los detalles. Aquí está el enlace: Pregunta
Renato Parreira
4

Los tipos que adoptan el protocolo URLRequestConvertible se pueden usar para construir solicitudes de URL.

Aquí hay un ejemplo tomado 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
  }
}

y podemos usar este ImmageRouter de la siguiente manera:

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

en lugar de caso UpdateUser (nombre de usuario: String, firstName: String, lastName: String, email: String)

harías

struct UserAttributes
{
    let username: String
     ....
}

y alimenta ESE objeto modelo como un parámetro en lugar de un grupo de cadenas ilegibles sin nombre

case UpdateUser (parámetros: UserAttributes)

Anton Tropashko
fuente