Ordnungsgemäße Verwendung des URLRequestConvertible von Alamofire

79

Ich habe einige Tutorials gelesen, README von @mattt, kann aber einige Dinge nicht herausfinden.

  1. Was ist die richtige Verwendung URLRequestConvertiblein der realen API? Es sieht so aus, als würde ich einen Router erstellen, indem ich ein URLRequestConvertibleProtokoll für alle APIs implementiere - es wird kaum lesbar sein. Sollte ich einen Router pro Endpunkt erstellen?

  2. Die zweite Frage ist höchstwahrscheinlich auf mangelnde Erfahrung mit der Sprache Swift zurückzuführen. Ich kann nicht herausfinden, warum ein enumRouter erstellt wird. Warum verwenden wir keine Klasse mit statischen Methoden? Hier ist ein Beispiel (aus Alamofires 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
        }
    }
    
  3. Es gibt zwei Möglichkeiten, Parameter zu übergeben:

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

    und (sagen Benutzer hat 4 Parameter)

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

    @mattt verwendet den ersten im Beispiel. Dies führt jedoch dazu, dass die Namen der Parameter außerhalb des Routers "hart codiert" werden (z. B. in UIViewControllern). Tippfehler im Parameternamen können zu Fehlern führen.
    Andere Leute verwenden die 2. Option, aber in diesem Fall ist es überhaupt nicht offensichtlich, was jeder Parameter darstellt.
    Was wird der richtige Weg sein, um es zu tun?

OgreSwamp
quelle

Antworten:

110

Tolle Fragen. Lassen Sie uns jeden einzeln aufschlüsseln.

Was ist die richtige Verwendung von URLRequestConvertible in der realen API?

Das URLRequestConvertibleProtokoll ist eine einfache Methode, um sicherzustellen, dass ein bestimmtes Objekt eine gültige erstellen kann NSURLRequest. Es gibt nicht wirklich strenge Regeln oder Richtlinien, die Sie dazu zwingen, dieses Protokoll auf eine bestimmte Weise zu verwenden. Es ist lediglich ein Komfortprotokoll, mit dem andere Objekte den Status speichern können, der zum ordnungsgemäßen Erstellen des Status erforderlich ist NSURLRequest. Weitere Informationen zu Alamofire finden Sie hier .

Sollte ich einen Router pro Endpunkt erstellen?

Definitiv nicht. Das würde den gesamten Zweck der Verwendung eines Enum. Swift Enum-Objekte sind erstaunlich leistungsfähig, sodass Sie eine große Menge gemeinsamer Zustände gemeinsam nutzen und die Teile einschalten können, die sich tatsächlich unterscheiden. Es NSURLRequestist wirklich mächtig , mit etwas so Einfachem wie dem Folgenden etwas erstellen zu können !

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

Ich kann nicht herausfinden, warum enum zum Erstellen von Routern verwendet wird. Warum verwenden wir keine Klasse mit statischen Methoden?

Eine Aufzählung wird verwendet, da dies eine viel präzisere Möglichkeit ist, mehrere verwandte Objekte unter einer gemeinsamen Schnittstelle auszudrücken. Alle Methoden werden von allen Fällen gemeinsam genutzt. Wenn Sie statische Methoden verwenden, müssen Sie für jeden Fall für jede Methode eine statische Methode haben. Oder Sie müssten eine Aufzählung im Obj-C-Stil innerhalb des Objekts verwenden. Hier ist ein kurzes Beispiel dafür, was ich meine.

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

Um die Methode eines der verschiedenen Endpunkte abzurufen, können Sie dieselbe Methode aufrufen, ohne Parameter übergeben zu müssen, um zu definieren, nach welchem ​​Endpunkttyp Sie suchen. Dies wird bereits von dem von Ihnen ausgewählten Fall behandelt.

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

Oder wenn Sie den Pfad erhalten möchten, die gleichen Arten von Anrufen.

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

Versuchen wir nun den gleichen Ansatz mit statischen Methoden.

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
}

HINWEIS: Wenn Sie nicht viele Eigenschaften oder Funktionen haben, die die Fälle aktivieren, bietet eine Aufzählung nicht viele Vorteile gegenüber einer Struktur. Es ist einfach ein alternativer Ansatz mit unterschiedlichem syntaktischem Zucker.

Aufzählungen können die Wiederverwendung von Status und Code maximieren. Mit den zugehörigen Werten können Sie auch einige wirklich leistungsstarke Aufgaben ausführen, z. B. Objekte gruppieren, die etwas ähnlich sind, aber unglaublich unterschiedliche Anforderungen haben ... wie z. B. die NSURLRequestErstellung.

Was ist der richtige Weg, um Parameter für Enum-Fälle zu erstellen, um die Lesbarkeit zu verbessern? (musste dieses zusammen pürieren)

Das ist eine tolle Frage. Sie haben bereits zwei mögliche Optionen festgelegt. Lassen Sie mich ein Drittel hinzufügen, das Ihren Bedürfnissen etwas besser entspricht.

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)

In Fällen, in denen Sie Werte zugeordnet haben, kann es hilfreich sein, explizite Namen für alle Werte im Tupel hinzuzufügen. Dies hilft wirklich dabei, den Kontext aufzubauen. Der Nachteil ist, dass Sie diese Werte dann in Ihren switch-Anweisungen wie folgt neu deklarieren müssen.

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

Dies gibt Ihnen zwar einen schönen, konsistenten Kontext, wird aber ziemlich ausführlich. Dies sind derzeit Ihre drei Optionen in Swift. Welche Option die richtige ist, hängt von Ihrem Anwendungsfall ab.


Aktualisieren

Mit der Veröffentlichung von 🔥🔥 Alamofire 4.0 🔥🔥 URLRequestConvertiblekann das jetzt VIEL schlauer sein und auch werfen. Wir haben Alamofire voll unterstützt, um ungültige Anforderungen zu verarbeiten und über die Antworthandler sinnvolle Fehler zu generieren. Dieses neue System ist in unserer README ausführlich dokumentiert .

cnoon
quelle
8
Vielen Dank. Nur eine Frage zu Ihrer Antwort zu einem Router im Vergleich zum Erstellen eines Routers pro Endpunkt (z. B. CRUD-Beispiel von der Alamofire-Seite). Denken Sie nicht, dass, wenn ich 5 Endpunkte sage, jeder 3-4 Methoden hat - das sind 15-20 caseAussagen. Das sieht für mich nach einer riesigen Methode aus. Ich bin nicht sicher, ob das zu dem lesbaren Code führen wird ...
OgreSwamp
1
In Bezug auf die zweite Antwort (Aufzählung vs. statische Methoden) besteht der springende Punkt für mich darin, eine Implementierung in Aufzählung / Klasse zu verbergen. Ich muss keine Methoden oder Pfade außerhalb kennen. Ich möchte anrufen Router.createUser("[email protected]", "....")und einen Block für die Interpretation der Ergebnisse für den Server haben. Alle Details (Methoden, Pfade, API-Root usw.) können für Router privat sein - das ist in Ordnung.
OgreSwamp
2
Zu Ihrem letzten Kommentar glaube ich nicht, dass Sie 20 verschiedene Endpunkte in eine einzige Aufzählung packen möchten, wenn Sie auch eine Reihe von Funktionen hätten. Ihre switch-Anweisungen wären so lang, dass sie nicht gut lesbar wären. Definitiv Code-Geruch an diesem Punkt. Wenn Sie mehr als 5 oder 6 Fälle in Ihren Schaltern haben, verlieren Sie für mich wirklich die Lesbarkeit.
cnoon
2
Zu Ihrem letzten Kommentar @cnoon (ich habe die vorherigen Kommentare gelesen) sagen Sie, dass (am Beispiel Ihres CRUD-Benutzerrouters), wenn ich einige Anfragen habe, die zu verschiedenen Kontexten gehören, wie z. B. Anfragen von Twitter und Benutzer-CRUD anfordern, diese werden zwei getrennte Router sein?
Renan Kosicki
3
Ja das stimmt @RenanKosicki. Sie erreichen mit Sicherheit einen Punkt ohne Wiederkehr, wenn Sie zu viele Fälle in einer Router-Aufzählung haben. Das Aufteilen in logische Gruppen ist sicherlich wünschenswerter.
cnoon
8

Hier ist das aktuelle enum Routerin Swift 3, das auf Alamofires Github empfohlen wird . Ich hoffe, Sie finden es nützlich, um einen Router ordnungsgemäß zu implementieren 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
quelle
7

Warum versuchen Sie nicht verwenden SweetRouter . Es hilft Ihnen dabei, alle Boilerplates zu entfernen, die Sie beim Deklarieren eines Routers haben, und es unterstützt auch Dinge wie mehrere Umgebungen, und Ihr Code ist wirklich lesbar.

Hier ist ein Beispiel für den Router mit süßem Router:

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
}

Und so würden Sie es verwenden:

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

Ich habe einen Weg gefunden, damit zu arbeiten. Ich habe eine Klasse mit dem Router darin erstellt: Klassen von einer Anfrage erben

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

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

Daher erhält die Sohnklasse vom übergeordneten Parameter die Router-Parameter, und Sie können Route.login sogar in jedem Sohn verwenden. Trotzdem weiß ich nicht, ob es eine Möglichkeit gibt, eine kurze URLRequest zu erhalten, daher muss ich keine Parameter immer wieder einstellen

David Alejandro Londoño Mejía
quelle
Hallo, ich versuche, das zu tun, was Sie in Ihrer Antwort gesagt haben, aber wenn ich versuche, die POST-Methode zu verwenden, erhalte ich immer noch die Antwort auf die GET-Methode. Beispiel: Wenn ich auf meine URL "/ users" zugreife, anstatt den Benutzer mit der POST-Methode zu erstellen, wird die Liste aller Benutzer angezeigt. Dies ist die Antwort der GET-Methode. Irgendeine Idee, warum das passiert? Scheint, als würde sich die Methode mutableURLRequest.HTTPMethod = method.rawValueohne Änderungen ändern.
Renato Parreira
Auf welche Aufzählung haben Sie zugegriffen? Sie müssen die Aufzählung für einen POST auswählen und einen POST für diesen Aufzählungswert festlegen. Hier lautet der Beitrag Router.setUser (...)
David Alejandro Londoño Mejía
Können Sie meine Frage hier in SO überprüfen? Dort gebe ich alle Details an. Hier ist der Link: Frage
Renato Parreira
4

Typen, die das URLRequestConvertible-Protokoll verwenden, können zum Erstellen von URL-Anforderungen verwendet werden.

Hier ist ein Beispiel von 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
  }
}

und wir können diesen ImmageRouter wie folgt verwenden:

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

anstelle von case UpdateUser (Benutzername: String, Vorname: String, Nachname: String, E-Mail: String)

du würdest machen

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

und füttere DIESES Modellobjekt als Parameter anstelle eines Clusters unbenannter unlesbarer Zeichenfolgen

case UpdateUser (Parameter: UserAttributes)

Anton Tropashko
quelle