Правильне використання URLRequestConvertible Alamofire


79

Я прочитав кілька підручників, README від @mattt, але не можу зрозуміти кілька речей.

  1. Яке правильне використання URLRequestConvertibleв реальному світі API? Схоже, якщо я буду створювати один маршрутизатор, впроваджуючи URLRequestConvertibleпротокол для всіх API - він буде ледь читабельним. Чи слід створювати по одному маршрутизатору на кінцеву точку?

  2. Друге питання, швидше за все, спричинене відсутністю досвіду роботи з мовою Swift. Я не можу зрозуміти, чому enumвикористовується для побудови маршрутизатора? Чому ми не використовуємо клас із статичними методами? ось приклад (з README 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. Існує 2 способи передачі параметрів:

    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). Помилка в назві параметра може призвести до помилки.
    Інші люди використовують другий варіант, але в такому випадку зовсім не очевидно, що представляє кожен параметр.
    Яким буде правильний спосіб це зробити?

Відповіді:


110

Чудові запитання. Давайте розберемо кожного окремо.

Яке належне використання URLRequestConvertible в реальному світі API?

URLRequestConvertibleПротокол є легкий спосіб забезпечити даний об'єкт може створити дійсні NSURLRequest. Насправді не існує суворого набору правил чи рекомендацій, які змушують вас використовувати цей протокол якимось особливим чином. Це просто протокол зручності, щоб дозволити іншим об'єктам зберігати стан, необхідний для правильного створення NSURLRequest. Дещо більше інформації, що стосується Alamofire, можна знайти тут .

Чи слід створювати по одному маршрутизатору на кінцеву точку?

Точно ні. Це перемогло б ціль використання Enum. Об'єкти Swift Enum надзвичайно потужні, що дозволяє вам ділитися великою кількістю загального стану та вмикати ті частини, які насправді відрізняються. Можливість створити NSURLRequestщось настільки просте, як наведене нижче, дійсно потужна!

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

Я не можу зрозуміти, чому enum використовується для побудови маршрутизатора? Чому ми не використовуємо клас із статичними методами?

Використовується перерахування, оскільки це набагато стисліший спосіб вираження декількох пов'язаних об'єктів під загальним інтерфейсом. Всі методи поділяються між усіма випадками. Якщо ви використовували статичні методи, вам потрібно було б мати статичний метод для кожного випадку для кожного методу. Або вам довелося б використовувати перелік стилів 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.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)"
        }
    }
}

Щоб отримати метод будь-якої з різних кінцевих точок, ви можете викликати той самий метод без необхідності передавати будь-які параметри, щоб визначити, який тип кінцевої точки ви шукаєте, це вже обробляється вибраним вами випадком.

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

ПРИМІТКА. Якщо у вас недостатньо властивостей або функцій, які вмикають регістри, перерахування не представляє багатьох переваг перед структурою. Це просто альтернативний підхід з різним синтаксичним цукром.

Перелічення можуть максимізувати повторне використання стану та коду. Пов’язані значення також дозволяють робити деякі дійсно потужні речі, такі як групування об’єктів, які чимось подібні, але мають неймовірно різні вимоги ... наприклад, NSURLRequestстворення.

Який правильний спосіб побудувати параметри для випадків перерахування для поліпшення читабельності? (довелося розім'яти цей разом)

Це приголомшливе питання. Ви вже виклали два можливі варіанти. Дозвольте додати третю частину, яка може дещо краще відповідати вашим потребам.

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, який з них є правильним, залежить від вашого випадку використання.


Оновлення

З виходом 🔥🔥 Alamofire 4.0 🔥🔥 URLRequestConvertibleтепер він може бути НАБАГАТО розумнішим, а також може кидати. Ми додали повну підтримку Alamofire для обробки недійсних запитів та генерування помилкових помилок за допомогою обробників відповідей. Ця нова система детально задокументована в нашому README .


8
Дякую. Лише одне запитання щодо вашої відповіді про один маршрутизатор проти побудови маршрутизатора на кінцеву точку (наприклад, CRUD-приклад зі сторінки Alamofire). Вам не здається, що якщо я маю, скажімо, 5 кінцевих точок, кожен має 3-4 методи, - це 15-20 caseтверджень. Для мене це виглядає величезним методом. Я не впевнений, що це призведе до читабельного коду ...
OgreSwamp

1
Що стосується другої відповіді (перелічення проти статичних методів) - для мене тут діркою є приховування реалізації всередині переліку / класу. Мені не потрібно знати методи чи шляхи поза ним. Я хочу зателефонувати Router.createUser("test@mail.com", "....")і мати блок для інтерпретації результатів для сервера. Всі деталі (методи, шляхи, корінь API тощо) можуть бути приватними для маршрутизатора - це нормально.
OgreSwamp

2
До вашого останнього коментаря, я не думаю, що ви хотіли б вкласти 20 різних кінцевих точок в один перелік, якби у вас також була купа функцій. Ваші оператори перемикання будуть настільки довгими, що будуть не дуже читабельними. Безумовно, запах коду в цей момент. Для мене, як тільки ви отримуєте понад 5 або 6 випадків у своїх комутаторах, ви справді починаєте втрачати читабельність.
полудень

2
До вашого останнього коментаря @cnoon, (я читав попередні коментарі), ви говорите, що (використовуючи ваш приклад маршрутизатора користувача CRUD), якщо у мене є якісь запити, що належать до різних контекстів, як Запит дописів із Twitter та Користувач CRUD, ті буде два окремих маршрутизатора?
Ренан Косіцький,

3
Так, це правильно @RenanKosicki. Ви, безумовно, досягнете точки неповернення, коли у вас буде занадто багато справ у переліку маршрутизаторів. Розбиття їх на логічні групи, безумовно, є більш бажаним дизайном.
вдень

8

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

7

Чому б вам не спробувати скористатися 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())))

4

Я знайшов спосіб працювати з ним, створив клас із маршрутизатором у ньому: успадковувати класи з запиту

файл 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
            }
        }
    }
}

файл 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
            }
        }
    }
}

таким чином клас son отримає параметри Router від батьків, і ви навіть можете використовувати Route.login у будь-якому сині. все-таки, не знаю, чи є спосіб отримати короткий URLRequest, тому мені не потрібно встановлювати параметри знову і знову


Привіт, я намагаюся зробити так, як ти сказав у своїй відповіді, але коли я намагаюся використовувати метод POST, я все одно отримую відповідь методу GET. Наприклад: Коли я отримую доступ до своєї URL-адреси "/ users", а не створюю користувача за допомогою методу POST, я отримую список усіх користувачів, що є відповіддю методу GET. Будь-яка ідея, чому це відбувається? Здається, навіть встановлення методу mutableURLRequest.HTTPMethod = method.rawValueнічого не змінюється.
Ренато Паррейра

до якого переліку Ви потрапили ?? вам потрібно вибрати перерахування для POST і встановити POST для цього значення переліку, тут пост Router.setUser (...)
Девід Алехандро Лондоньо Мехія

Чи можете ви перевірити моє запитання тут у SO? Там я надаю всі деталі. Ось посилання: Запитання
Ренато Паррейра

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

0

замість регістру UpdateUser (ім'я користувача: String, firstName: String, lastName: String, email: String)

ви зробите

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

і подати об’єкт ТОЇ моделі як параметр замість кластеру неіменованих нечитабельних рядків

справа UpdateUser (параметри: UserAttributes)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.