I would like to post binary data through RxAlamofire, Alamofire or even without any library but after some days of research and tries, I'm not able to do it.
Here you can find the POSTMAN example of the request that I am trying to reproduce is:
Is a post method with the Authorization and Content-Type headers and the binary data attached.
I have tried to find some example or something related but I couldn't find a solution. I could just find multipart form data examples but with multipart form data the server doesn't work (is a external API)
If someone could guide me or show me some example code.
Here the code used for login as example and to show you something that I want to achieve:
public class APIClient: DataSource {
public static var shared: APIClient = APIClient()
private init(){}
public func login(email:String, password:String) -> Observable<LoginResponse> {
return RxAlamofire.requestJSON(APIRouter.login(email:email, password:password))
.subscribeOn(MainScheduler.asyncInstance)
.debug()
.mapObject(type: LoginResponse.self)
}
}
Here the LoginResponse object:
public struct LoginResponse: Mappable {
var tokenId: String?
var userId: String?
public init?(map: Map) {}
public mutating func mapping(map: Map) {
tokenId <- map["id"]
userId <- map["userId"]
}
}
And finally the APIRouter extending URLRequestConvertible:
enum APIRouter: URLRequestConvertible {
case login(email: String, password: String)
private var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
private var path: String {
switch self {
case .login:
return "users/login"
}
}
private var parameters: Parameters? {
switch self {
case .login(let email, let password):
return [APIConstants.LoginParameterKey.email: email, APIConstants.LoginParameterKey.password: password]
}
}
private var query: [URLQueryItem]? {
var queryItems = [URLQueryItem]()
switch self {
case .login:
return nil
}
}
func asURLRequest() throws -> URLRequest {
var urlComponents = URLComponents(string: APIConstants.ProductionServer.baseURL)!
if let query = query {
urlComponents.queryItems = query
}
var urlRequest = URLRequest(url: urlComponents.url!.appendingPathComponent(path))
// HTTP Method
urlRequest.httpMethod = method.rawValue
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
if let parameters = parameters {
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
}
return urlRequest
}
}
Thank you in advance!
EDIT To convert into RxAlamofire
With the code below I could solve the problem and convert it into RxSwift but I would like to use RxAlamofire to obtain the same result:
public func upload(media: Data) -> Observable<ContentUri> {
let headers = [
"content-type": "image/png",
"authorization": "token header"
]
return Observable<ContentUri>.create({observer in
Alamofire.upload(media, to: "\(endPoint)/api/media/upload", headers: headers)
.validate()
.responseJSON { response in
print(response)
}
return Disposables.create();
})
}
Alamofire.upload() (which returns an UploadRequest) might do what you want:
let headers = [
"Content-Type":"image/jpeg",
"Authorization":"sometoken",
]
let yourData = ... // Data of your image you want to upload
let endPoint = ...
Alamofire.upload(yourData, to: "\(endPoint)/api/media/upload", headers: headers)
.validate(statusCode: 200..<300)
.responseJSON { response in
// handle response
}
This example does not include RxAlamofire - but I am pretty sure it has a similar upload function. I hope it helps!
Related
I am following along with this tutorial in order to create an async generic network layer. I got the network manager working correctly.
https://betterprogramming.pub/async-await-generic-network-layer-with-swift-5-5-2bdd51224ea9
As I try to implement more APIs, that I can use with the networking layer, some of the APIs require different tokens, different content in the body, or header etc, that I have to get at runtime.
In the snippet of code below from the tutorial, I get that we are building up the Movie endpoint based on .self, and then return the specific values we need. But the issue is, some of the data in this, for example, the access token, has to be hard coded here. I am looking for a way, that I can 'inject' the accessToken, and then it will be created with this new token. Again, the reason for this, is that in other APIs, the access token might not always be known.
protocol Endpoint {
var scheme: String { get }
var host: String { get }
var version: String? { get }
var path: String { get }
var method: RequestMethod { get }
var queryItems: [String: String]? { get }
var header: [String: String]? { get }
var body: [String: String]? { get }
}
extension MoviesEndpoint: Endpoint {
var path: String {
switch self {
case .topRated:
return "/3/movie/top_rated"
case .movieDetail(let id):
return "/3/movie/\(id)"
}
}
var method: RequestMethod {
switch self {
case .topRated, .movieDetail:
return .get
}
}
var header: [String: String]? {
// Access Token to use in Bearer header
let accessToken = "insert your access token here -> https://www.themoviedb.org/settings/api"
switch self {
case .topRated, .movieDetail:
return [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8"
]
}
}
var body: [String: String]? {
switch self {
case .topRated, .movieDetail:
return nil
}
}
For an example, I tried converting the var body to a function, so I could do
func body(_ bodyDict: [String, String]?) -> [String:String]? {
switch self{
case .test:
return bodyDict
}
The idea of above, was that I changed it to a function, so I could pass in a dict, and then return that dict in the api call, but that did not work. The MoviesEnpoint adheres to the extension Endpoint, which then gives the compiler error 'Protocol Methods must not have bodies'.
Is there a way to dependency inject runtime parameters into this Extension/Protocol method?
Change the declaration of MoviesEndpoint so that it stores the access token:
struct MoviesEndpoint {
var accessToken: String
var detail: Detail
enum Detail {
case topRated
case movieDetail(id: Int)
}
}
You'll need to change all the switch self statements to switch detail.
However, I think the solution in the article (four protocols) is overwrought.
Instead of a pile of protocols, make one struct with a single function property:
struct MovieDatabaseClient {
var getRaw: (MovieEndpoint) async throws -> (Data, URLResponse)
}
Extend it with a generic method to handle the response parsing and decoding:
extension MovieDatabaseClient {
func get<T: Decodable>(
endpoint: MovieEndpoint,
as responseType: T.Type = T.self
) async throws -> T {
let (data, response) = try await getRaw(endpoint)
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
switch response.statusCode {
case 200...299:
break
case 401:
throw URLError(.userAuthenticationRequired)
default:
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(responseType, from: data)
}
}
Provide a “live“ implementation that actually sends network requests:
extension MovieDatabaseClient {
static func live(host: String, accessToken: String) -> Self {
return .init { endpoint in
let request = try liveURLRequest(
host: host,
accessToken: accessToken,
endpoint: endpoint
)
return try await URLSession.shared.data(for: request)
}
}
// Factored out in case you want to write unit tests for it:
static func liveURLRequest(
host: String,
accessToken: String,
endpoint: MovieEndpoint
) throws -> URLRequest {
var components = URLComponents()
components.scheme = "https"
components.host = host
components.path = endpoint.urlPath
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8",
]
return request
}
}
extension MovieEndpoint {
var urlPath: String {
switch self {
case .topRated: return "/3/movie/top_rated"
case .movieDetail(id: let id): return "/3/movie/\(id)"
}
}
}
To use it in your app:
// At app startup...
let myAccessToken = "loaded from UserDefaults or something"
let client = MovieDatabaseClient.live(
host: "api.themoviedb.org",
accessToken: myAccessToken
)
// Using it:
let topRated: TopRated = try await client.get(endpoint: .topRated)
let movieDetail: MovieDetail = try await client.get(endpoint: .movieDetail(id: 123))
For testing, you can create a mock client by providing a single closure that fakes the network request/response. Simple examples:
extension MovieDatabaseClient {
static func mockSuccess<T: Encodable>(_ body: T) -> Self {
return .init { _ in
let data = try JSONEncoder().encode(body)
let response = HTTPURLResponse(
url: URL(string: "test")!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: nil
)!
return (data, response)
}
}
static func mockFailure(_ error: Error) -> Self {
return .init { _ in
throw error
}
}
}
So a test can create a mock client that always responds with a TopRated response like this:
let mockTopRatedClient = MovieDatabaseClient.mockSuccess(TopRated(...))
If you want to learn more about this style of dependency management and mocking, Point-Free has a good (but subscription required) series of episodes: Designing Dependencies.
I'm currently making a migration from Android to iOS, better said Java to Swift, I got a generic response in JSON, but I'm not able to use it as an object and show it in the storyboard. I'm really new to Swift so I've been stuck for a while.
I've tried ObjectMapper and also JSON decode with no result at all.
I declared this response as I used in Java(Android)
class ResponseObjectMapper<T,R>: Mappable where T: Mappable,R:Mappable{
var data:T?
var message:String!
var error:R?
required init?(_ map: Map) {
self.mapping(map: map)
}
func mapping(map: Map) {
data <- map["data"]
message <- map["message"]
error <- map["error"]
}
}
class UserMapper :Mappable{
var email:String?
var fullName:String?
var id:CLong?
var phoneNumber:String?
var token:CLong?
required init?(_ map: Map) {
}
func mapping(map: Map) {
email <- map["email"]
fullName <- map["fullName"]
id <- map["id"]
phoneNumber <- map["phoneNumber"]
token <- map["token"]
phoneNumber <- map["phoneNumber"]
}
}
In my Android project I use the Gson dependency and I was able to use my JSON as an object
class ErrorMapper:Mappable{
var message:String?
var code:Int?
required init?(_ map: Map) {
}
func mapping(map: Map) {
message <- map["message"]
code <- map["code"]
}
}
This is the Alamofire that gave me the JSON.
func login(params: [String:Any]){Alamofire.request
("http://192.168.0.192:8081/SpringBoot/user/login", method: .post,
parameters: params,encoding: JSONEncoding.default, headers:
headers).responseJSON {
response in
switch response.result {
case .success:
let response = Mapper<ResponseObjectMapper<UserMapper,ErrorMapper>>.map(JSONString: response.data)
break
case .failure(let error):
print(error)
}
}
}
If I print the response with print(response) I got
SUCCESS: {
data = {
email = "vpozo#montran.com";
fullName = "Victor Pozo";
id = 6;
phoneNumber = 099963212;
token = 6;
};
error = "<null>";
message = SUCCESS;
}
and if I use this code I can got a result with key and value but I don't know how to use it as an object
if let result = response.result.value {
let responseDict = result as! [String : Any]
print(responseDict["data"])
}
console:
Optional({
email = "vpozo#gmail.com";
fullName = "Victor Pozo";
id = 6;
phoneNumber = 099963212;
token = 6;
})
I would like to use it in an Object, like user.token in a View Controller, probably I'm really confused, trying to map with generic attributes.
Type 'ResponseObjectMapper<UserMapper, ErrorMapper>' does not conform to protocol 'BaseMappable'
First of all you will need a Network Manager which uses Alamofire to make all your requests. I have made generalized one that looks something like this. You can modify it as you want.
import Foundation
import Alamofire
import SwiftyJSON
class NetworkHandler: NSObject {
let publicURLHeaders : HTTPHeaders = [
"Content-type" : "application/json"
]
let privateURLHeaders : HTTPHeaders = [
"Content-type" : "application/json",
"Authorization" : ""
]
enum RequestType {
case publicURL
case privateURL
}
func createNetworkRequestWithJSON(urlString : String , prametres : [String : Any], type : RequestType, completion:#escaping(JSON) -> Void) {
let internetIsReachable = NetworkReachabilityManager()?.isReachable ?? false
if !internetIsReachable {
AlertViewManager.sharedInstance.showAlertFromWindow(title: "", message: "No internet connectivity.")
} else {
switch type {
case .publicURL :
commonRequest(urlString: baseURL+urlString, parameters: prametres, completion: completion, headers: publicURLHeaders)
break
case .privateURL:
commonRequest(urlString: baseURL+urlString, parameters: prametres, completion: completion, headers: privateURLHeaders)
break
}
}
}
func commonRequest(urlString : String, parameters : [String : Any], completion : #escaping (JSON) -> Void , headers : HTTPHeaders){
print("urlString:"+urlString)
print("headers:")
print(headers)
print("parameters:")
print(parameters)
let url = NSURL(string: urlString)
var request = URLRequest(url: url! as URL)
request.httpMethod = "POST"
request.httpHeaders = headers
request.timeoutInterval = 10
let data = try! JSONSerialization.data(withJSONObject: parameters, options: JSONSerialization.WritingOptions.prettyPrinted)
let json = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
if let json = json {
print("parameters:")
print(json)
}
request.httpBody = json!.data(using: String.Encoding.utf8.rawValue)
let alamoRequest = AF.request(request as URLRequestConvertible)
alamoRequest.validate(statusCode: 200..<300)
alamoRequest.responseJSON{ response in
print(response.response?.statusCode as Any )
if let status = response.response?.statusCode {
switch(status){
case 201:
print("example success")
SwiftLoader.hide()
case 200 :
if let json = response.value {
let jsonObject = JSON(json)
completion(jsonObject)
}
default:
SwiftLoader.hide()
print("error with response status: \(status)")
}
}else{
let jsonObject = JSON()
completion(jsonObject)
SwiftLoader.hide()
}
}
}
}
After this when ever you need to make a request you can use this function. This will take in parameters if any needed and once the request is complete it will execute a call back function in which you can handle the response. The response here will be of SWIFTYJSON format.
func makeNetworkRequest(){
let networkHandler = NetworkHandler()
var parameters : [String:String] = [:]
parameters["email"] = usernameTextField.text
parameters["pass"] = passwordTextField.text
networkHandler.createNetworkRequestWithJSON(urlString: "http://192.168.0.192:8081/SpringBoot/user/login", prametres: parameters, type: .publicURL, completion: self.handleResponseForRequest)
}
func handleResponseForRequest(response: JSON){
if let message = response["message"].string{
if message == "SUCCESS"{
if let email = response["data"]["email"].string{
//Do something with email.
}
if let fullName = response["data"]["fullName"].string{
//Do something with fullName.
}
if let id = response["data"]["id"].int{
//Do something with id.
}
if let phoneNumber = response["data"]["phoneNumber"].int64{
//Do something with phoneNumber.
}
if let token = response["data"]["token"].int{
//Do something with token.
}
}else{
//Error
}
}
}
Hope this helps. Let me know if you get stuck anywhere.
For my networking module, I have this protocol that I adopt for accessing different parts of the API:
protocol Router: URLRequestConvertible {
var baseUrl: URL { get }
var route: Route { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var encoding: ParameterEncoding? { get }
var responseResultType: Decodable.Type? { get }
}
I'm adopting this with enums that look like this:
enum TestRouter: Router {
case getTestData(byId: Int)
case updateTestData(byId: Int)
var route: Route {
switch self {
case .getTestData(let id): return Route(path: "/testData/\(id)")
case .updateTestData(let id): return Route(path: "/testDataOtherPath/\(id)")
}
}
var method: HTTPMethod {
switch self {
case .getTestData: return .get
case .updateTestData: return .put
}
}
var headers: [String : String]? {
return [:]
}
var encoding: ParameterEncoding? {
return URLEncoding.default
}
var responseResultType: Decodable.Type? {
switch self {
case .getTestData: return TestData.self
case .updateTestData: return ValidationResponse.self
}
}
}
I want to use Codable for decoding nested Api responses. Every response consists of a token and a result which content is depending on the request route.
For making the request I want to use the type specified in the responseResultType property in the enum above.
struct ApiResponse<Result: Decodable>: Decodable {
let token: String
let result: Result
}
extension Router {
func asURLRequest() throws -> URLRequest {
// Construct URL
var completeUrl = baseUrl.appendingPathComponent(route.path, isDirectory: false)
completeUrl = URL(string: completeUrl.absoluteString.removingPercentEncoding ?? "")!
// Create URL Request...
var urlRequest = URLRequest(url: completeUrl)
// ... with Method
urlRequest.httpMethod = method.rawValue
// Add headers
headers?.forEach { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) }
// Encode URL Request with the parameters
if encoding != nil {
return try encoding!.encode(urlRequest, with: route.parameters)
} else {
return urlRequest
}
}
func requestAndDecode(completion: #escaping (Result?) -> Void) {
NetworkAdapter.sessionManager.request(urlRequest).validate().responseData { response in
let responseObject = try? JSONDecoder().decode(ApiResponse<self.responseResultType!>, from: response.data!)
completion(responseObject.result)
}
}
}
But in my requestAndDecode method It throws an compiler error (Cannot invoke 'decode' with an argument list of type '(Any.Type, from: Data)'). I can't use ApiResponse<self.responseResultType!> like that.
I could make this function generic and call it like this:
TestRouter.getTestData(byId: 123).requestAndDecode(TestData.self, completion:)
but then I'd have to pass the response type everytime I want to use this endpoint.
What I want to achieve is that the extension function requestAndDecode takes it response type information from itself, the responseResultType property.
Is this possible?
Ignoring the actual error report you have a fundamental problem with requestAndDecode: it is a generic function whose type parameters are determined at the call site which is declared to return a value of type Result yet it attempts to return a value of type self.responseResultType whose value is an unknown type.
If Swift's type system supported this it would require runtime type checking, potential failure, and your code would have to handle that. E.g. you could pass TestData to requestAndDecode while responseResultType might be ValidationResponse...
Change the JSON call to:
JSONDecoder().decode(ApiResponse<Result>.self ...
and the types statically match (even though the actual type that Result is is unknown).
You need to rethink your design. HTH
Create a Generic function with Combine and AlomFire. You can use it for all method(get, post, put, delete)
func fatchData<T: Codable>(requestType: String, url: String, params: [String : Any]?, myType: T.Type, completion: #escaping (Result<T, Error>) -> Void) {
var method = HTTPMethod.get
switch requestType {
case "Get":
method = HTTPMethod.get
case "Post":
method = HTTPMethod.post
print("requestType \(requestType) \(method) ")
case "Put":
method = HTTPMethod.put
default:
method = HTTPMethod.delete
}
print("url \(url) \(method) \(AppConstant.headers) ")
task = AF.request(url, method: method, parameters: params, encoding: JSONEncoding.default, headers: AppConstant.headers)
.publishDecodable(type: myType.self)
.sink(receiveCompletion: { (completion) in
switch completion{
case .finished:
()
case .failure(let error):
// completion(.failure(error))
print("error \(error)")
}
}, receiveValue: {
[weak self ](response) in
print("response \(response)")
switch response.result{
case .success(let model):
completion(.success(model))
print("error success")
case .failure(let error):
completion(.failure(error))
print("error failure \(error.localizedDescription)")
}
}
)
}
I've been fighting this for over 10 hours and done my best before coming here. Many tutorials I've looked out ended up being outdated by a version or two and/or missing some key knowledge in the explanation.
I'm trying to upload a PDF to a server using a WCF Rest Service. When I debug on the WCF service, the variable named document is a null stream. I've searched the web many hours trying many different things and I've exhausted MANY attempts to get such a little thing to work! What is really annoying is that this isn't the first time someone has needed to do this and yet I haven't found an answer anywhere.
If there is a better way to post the PDF other than what I've posted, I'm open to suggestions but I don't want to use third-party frameworks.
Requirements:
- Using the WCF service is required
- I'm not saving to a file system or UNC
I need to get a valid memory stream passed to the service and I can take care of the function code from there. I've tried using a Base64DataString before this and I could get that to work either. If you wanted to provide that as an option, I'm open to it.
Please help!
Swift 4 code:
import PDFKit
class FileUpload {
static func generateBoundaryString() -> String {
return "Boundary-\(UUID().uuidString)"
}
static func dataUploadBodyWithParameters(_ parameters: [String: Any]?, filename: String, mimetype: String, dataKey: String, data: Data, boundary: String) -> Data {
var body = Data()
// encode parameters first
if parameters != nil {
for (key, value) in parameters! {
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.appendString("\(value)\r\n")
}
}
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"\(dataKey)\"; filename=\"\(filename)\"\r\n")
body.appendString("Content-Type: \(mimetype)\r\n\r\n")
body.append(data)
body.appendString("\r\n")
body.appendString("--\(boundary)--\r\n")
print(body)
return body
}
static func uploadData(_ data: Data, toURL urlString: String, withFileKey fileKey: String, completion: ((_ success: Bool, _ result: Any?) -> Void)?) {
if let url = URL(string: urlString) {
// build request
let boundary = generateBoundaryString()
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// build body
let body = dataUploadBodyWithParameters(nil, filename: "iOSUpload.pdf", mimetype: "application/pdf", dataKey: fileKey, data: data, boundary: boundary)
request.httpBody = body
//UIApplication.shared.isNetworkActivityIndicatorVisible = true
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if data != nil && error == nil {
do {
let result = try JSONSerialization.jsonObject(with: data!, options: [])
print(result)
DispatchQueue.main.async(execute: { completion?(true, result) })
} catch {
DispatchQueue.main.async(execute: { completion?(false, nil) })
}
} else { DispatchQueue.main.async(execute: { completion?(false, nil) }) }
//UIApplication.shared.isNetworkActivityIndicatorVisible = false
}).resume()
} else { DispatchQueue.main.async(execute: { completion?(false, nil) }) }
}
}
extension Data {
mutating func appendString(_ string: String) {
let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true)
append(data!)
}
}
}
WCF Relevant Code:
<ServiceContract>
Public Interface IWebService
<OperationContract>
<WebInvoke(Method:="POST", UriTemplate:="UploadFile/{fileName}")>
Function UploadFile(fileName As String, document As Stream) As String
End Interface
<ServiceBehavior(InstanceContextMode:=InstanceContextMode.Single)>
Public Class MyWebService : Implements IWebService
Public Function UploadFile(fileName As String, document As Stream) As String Implements IWebService.UploadFile
'document is null
End Function
This is a 2 part question the first is similar to this question here: Proper usage of the Alamofire's URLRequestConvertible. But I need a little more help!
1) Do I create an enum router which implements URLRequestConvertible for each model in my model layer?
The alamofire github page provides an example of a router which i've copied here:
enum Router: URLRequestConvertible {
static let baseURLString = "http://example.com"
static var OAuthToken: String?
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)"
}
}
// MARK: URLRequestConvertible
var URLRequest: NSURLRequest {
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 .CreateUser(let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateUser(_, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
}
When I look at this (i'm new at swift so please bear with me >_<) I see operations on a user object; they are creating a user, updating a user etc... So, if I had model objects person, company, location in my model layer, would I create a router for each model object?
2) When interacting heavily with an API, I'm used to creating a "network manager" singleton to abstract away the network layer and to hold headers and the baseurl for that API. The alamofire has a "Manager" described here:
Top-level convenience methods like Alamofire.request use a shared instance of Alamofire.Manager, which is configured with the default NSURLSessionConfiguration. As such, the following two statements are equivalent:
Alamofire.request(.GET, "http://httpbin.org/get")
let manager = Alamofire.Manager.sharedInstance
manager.request(NSURLRequest(URL: NSURL(string: "http://httpbin.org/get")))
is this manager what I should be using as my singleton? If so, how do I set the baseurl on the manager? Also, if I use this manager does / can this work together with the router construct shown above (with each model object setting it's baseurl and NSURLRquest)? If so can you provide a simple example?
I'm new to the Alamofire library and swift. So, I know there are a lot of holes in my understanding but I'm just trying to understand the best that I can! Any info helps. Thanks.
These are some really good questions. Let me attempt to answer each one in turn.
Do I create an enum router which implements URLRequestConvertible for each model in my model layer?
This is a great question and unfortunately there's no one perfect answer. There are certainly some ways that you could extend the Router pattern to accommodate multiple object types. The first option would be to add more cases to support another object type. However, this gets hairy pretty quickly when you get more than 6 or 7 cases. Your switch statements just start to get out-of-control. Therefore, I wouldn't recommend this approach.
Another way to approach the problem is by introducing generics into the Router.
RouterObject Protocol
protocol RouterObject {
func createObjectPath() -> String
func readObjectPath(identifier: String) -> String
func updateObjectPath(identifier: String) -> String
func destroyObjectPath(identifier: String) -> String
}
Model Objects
struct User: RouterObject {
let rootPath = "/users"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
struct Company: RouterObject {
let rootPath = "/companies"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
struct Location: RouterObject {
let rootPath = "/locations"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
Router
let baseURLString = "http://example.com"
var OAuthToken: String?
enum Router<T where T: RouterObject>: URLRequestConvertible {
case CreateObject(T, [String: AnyObject])
case ReadObject(T, String)
case UpdateObject(T, String, [String: AnyObject])
case DestroyObject(T, String)
var method: Alamofire.Method {
switch self {
case .CreateObject:
return .POST
case .ReadObject:
return .GET
case .UpdateObject:
return .PUT
case .DestroyObject:
return .DELETE
}
}
var path: String {
switch self {
case .CreateObject(let object, _):
return object.createObjectPath()
case .ReadObject(let object, let identifier):
return object.readObjectPath(identifier)
case .UpdateObject(let object, let identifier, _):
return object.updateObjectPath(identifier)
case .DestroyObject(let object, let identifier):
return object.destroyObjectPath(identifier)
}
}
// MARK: URLRequestConvertible
var URLRequest: NSMutableURLRequest {
let URL = NSURL(string: baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
if let token = OAuthToken {
mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
switch self {
case .CreateObject(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateObject(_, _, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
}
Example Usage
func exampleUsage() {
let URLRequest = Router.CreateObject(Location(), ["address": "1234 Road of Awesomeness"]).URLRequest
Alamofire.request(URLRequest)
.response { request, response, data, error in
print(request)
print(response)
print(data)
print(error)
}
}
Now there are certainly a few tradeoffs that you have to make here. First off, your model objects need to conform to the RouterObject protocol. Otherwise the Router has no idea what to use for the path. Also, you'll need to make sure all your paths can be constructed with the a single identifier. If they cannot, this design might not work. The last issue is that you cannot store the baseURL or the OAuthToken directly inside the Router enum. Unfortunately, static and stored properties are not yet supported in generic enumerations.
Regardless, this would certainly be a valid way to avoid having to create a Router for every model object.
Should the Alamofire.Manager.sharedInstance be used as my singleton NetworkManager instance?
It certainly could be used in that fashion. It really depends upon your use case and how you have designed your network access. It also depends on how many different types of sessions you need. If you need background sessions and default sessions, then you probably still need the concept of a NetworkManager that contains each custom Manager instance. However, if you are just hitting the network with a default session, then the sharedInstance would probably be sufficient.
How could the baseURL of the Alamofire singleton be used in conjunction with the Router pattern?
Good question...the code below is one example of how it could be done.
Alamofire Manager extension
extension Manager {
static let baseURLString = "http://example.com"
static var OAuthToken: String?
}
Router URLRequestConvertible Updates
var URLRequest: NSMutableURLRequest {
let URL = NSURL(string: Alamofire.Manager.baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
if let token = Alamofire.Manager.OAuthToken {
mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
switch self {
case .CreateObject(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateObject(_, _, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
Hopefully that helps shed some light. Best of luck!