Правильное использование URL-адреса AlamofireConvertible

Я прочитал пару учебных пособий, README от @mattt, но не могу понять, что с ними связано.

  • Какое правильное использование URLRequestConvertible в API реального мира? Похоже, если я создам один маршрутизатор, выполнив протокол URLRequestConvertible для всех API - он будет едва читаемым. Должен ли я создать один маршрутизатор на конечную точку?

  • Второй вопрос, скорее всего, вызванный отсутствием опыта с языком Swift. Я не могу понять, почему enum используется для построения маршрутизатора? Почему мы не используем класс со статическими методами? вот пример (из Alamofire README)

    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
        }
    }
    
  • Есть два способа передачи параметров:

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

    и (скажем, пользователь имеет 4 параметра)

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

    @mattt использует первый пример. Но это приведет к именам параметров "жесткого кодирования" за пределами маршрутизатора (например, в UIViewControllers). Typo в имени параметра может привести к ошибке.
    Другие люди используют 2-й вариант, но в этом случае это совершенно не очевидно, что представляет каждый параметр.
    Каким будет правильный способ?

Ответ 1

Отличные вопросы. Пусть каждый раз разбивается каждый.

Какое правильное использование URLRequestConvertible в реальном мире API?

Протокол URLRequestConvertible - это легкий способ гарантировать, что данный объект может создать допустимый NSURLRequest. На самом деле не существует строгого набора правил или указаний, которые вынуждают вас использовать этот протокол каким-либо определенным образом. Это просто удобный протокол, позволяющий другим объектам сохранять состояние, необходимое для правильного создания NSURLRequest. Более подробную информацию об Alamofire можно найти здесь.

Должен ли я создать один маршрутизатор на конечную точку?

Определенно нет. Это нарушит всю цель использования Enum. Объекты Swift Enum удивительно мощны, что позволяет вам делиться большим количеством общего состояния и включать те части, которые на самом деле разные. Возможность создания NSURLRequest с чем-то простеньким, чем следующее, действительно мощное!

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

Я не могу понять, почему перечисление используется для построения маршрутизатора? Почему мы не используем класс со статическими методами?

Используется перечисление, потому что это гораздо более сжатый способ выражения нескольких связанных объектов под общим интерфейсом. Все методы распределяются между всеми случаями. Если вы использовали статические методы, для каждого метода для каждого метода должен быть статический метод. Или вам придется использовать обводку в стиле Obj-C внутри объекта. Вот краткий пример того, что я имею в виду.

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.Method {
        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)"
        }
    }
}

Чтобы получить метод любой из разных конечных точек, вы можете вызывать один и тот же метод без необходимости передавать какие-либо параметры для определения того, какой тип конечной точки вы ищете, он уже обрабатывается выбранным вами случаем.

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

Или, если вы хотите получить путь, те же типы вызовов.

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

Теперь попробуйте использовать тот же подход, используя статические методы.

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 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
}

ПРИМЕЧАНИЕ. Если у вас нет многих свойств или функций, которые включают случаи, то перечисление не предоставляет много преимуществ над структурой. Это просто альтернативный подход с различным синтаксическим сахаром.

Перечисления могут максимизировать повторное использование состояния и кода. Связанные значения также позволяют делать некоторые действительно мощные вещи, такие как группирование объектов, которые несколько схожи, но имеют невероятно разные требования... например, NSURLRequest creation.

Каков правильный способ создания параметров для перечислений для улучшения удобочитаемости? (пришлось смять это вместе)

Это потрясающий вопрос. Вы уже определили два возможных варианта. Позвольте мне добавить третью, которая может удовлетворить ваши потребности немного лучше.

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)

В случаях, когда у вас есть связанные значения, я думаю, что может быть полезно добавить явные имена для всех значений в кортеже. Это действительно помогает построить контекст. Недостатком является то, что вам придется переопределить эти значения в операторах коммутатора, как это.

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

Пока это дает вам приятный, последовательный контекст, он становится довольно многословным. Это ваши три варианта на данный момент в Swift, который правильный для использования зависит от вашего варианта использования.


Update

С выпуском 🔥🔥 Alamofire 4.0 🔥🔥 URLRequestConvertible теперь может быть намного умнее и может также бросать. Мы добавили полную поддержку Alamofire для обработки недопустимых запросов и создания разумных ошибок с помощью обработчиков ответов. Эта новая система подробно описана в нашем README.

Ответ 2

Почему бы вам не попробовать использовать SweetRouter. Это поможет вам удалить весь шаблон, который у вас есть при объявлении маршрутизатора, а также поддерживает такие вещи, как несколько сред, и ваш код будет действительно понятным.

Вот пример маршрутизатора со сладким маршрутизатором:

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
}

И вот как вы его используете:

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

Ответ 3

Я нашел способ работать с ним, я создал класс с Router в нем: наследовать классы из запроса

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
            }
        }
    }
}

запрос файлаContacts.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
            }
        }
    }
}

поэтому класс сына получит параметры родителя от родителя, и вы даже можете использовать Route.login в любом сыне. все еще, не знаю, есть ли способ получить короткий URLRequest, поэтому мне не нужно устанавливать параметры снова и снова

Ответ 4

Типы, использующие протокол URLRequestConvertible, могут использоваться для создания запросов URL.

Вот пример, взятый из 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
  }
}

и мы можем использовать этот ImmageRouter следующим образом:

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

Ответ 5

Вот обновленный enum Router в Swift 3, который рекомендуется на Alamofire Github. Надеюсь, вы сочтете это полезным с точки зрения правильного внедрения маршрутизатора с 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
    }
}