File upload with alamofire Router - swift

How can i upload a file with the alamofire Router? Struggling with it.
I'm also interested how i can upload file and parameters in the same request with Alamofire router.
Thanks!

I upload an image using the following code. For a given file type, you should be able to adjust the multipartEncoding block accordingly.
let json: [String : Any] = ["key", "value"]
let image = UIImage(named: "Something")!
guard let imageData = UIImagePNGRepresentation(image) else {
// We couldn't convert the image into data :(
}
let multipartEncoding: (MultipartFormData) -> Void = { multipartFormData in
multipartFormData.append(imageData, withName: "image", fileName: "image.png", mimeType: "image/png")
}
let request = Router.uploadPicture(parameters: json) // This is just a plain POST
sessionManager.upload(
multipartFormData: multipartEncoding,
with: request,
encodingCompletion: { (encodingResult) in
switch encodingResult {
case .success(let request, _, _):
request.validate().responseJSON(completionHandler: { (dataResponse) in
if let error = dataResponse.result.error {
// Network request failed :(
}
// SUCCESS!
})
case .failure(_):
// Couldn't encode the image :(
}
})
This is assuming your Router looks something like this.
enum Router: URLRequestConvertible {
case uploadPicture(parameters: Parameters)
static var baseUrl = "https://website.com/"
var method: HTTPMethod {
switch self {
case .uploadPicture(_):
return .post
}
}
var path: String {
switch self {
case .uploadPicture(_):
return "api/upload-picture/"
}
}
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try Router.baseUrl.asURL().appendingPathComponent(path)
var urlRequest = try URLRequest(url: url, method: method)
var encodedUrlRequest: URLRequest?
switch self {
case .uploadPicture(let parameters):
encodedUrlRequest = try? JSONEncoding.default.encode(urlRequest, with: parameters)
}
return encodedUrlRequest ?? urlRequest
}
}

Related

How to modify redirect body with Moya? (iOS, Swift, Moya, Alamofire)

I have an API, https://00.00.00.00/api/commitPayment that in the body has amount value. When request sends, server redirect this request to https://00.00.00.00/api/commitPaymentV3. But redirect has empty body. I need to modify redirect and pass amount from request to redirected request. Address in redirect may change, so I can't just sent requests directly to it.
Request
struct TopUpWithAmountRequest: Codable {
let amount: Int
let redirect: Bool
enum CodingKeys: String, CodingKey {
case amount = "Amount"
case redirect = "ShouldRedirect"
}
}
API
import Moya
enum TopUpAPI {
case topUpWithAmount(request: TopUpWithAmountRequest)
}
extension TopUpAPI: TargetType {
var baseURL: URL {
return URL(string: "https://00.00.00.00")!
}
var path: String {
switch self {
case .topUpWithAmount:
return "/api/commitPayment"
}
var method: Moya.Method {
switch self {
case .topUpWithAmount:
return .post
}
var task: Task {
switch self {
case .topUpWithAmount(let request):
return .requestJSONEncodable(request)
}
var headers: [String : String]? {
let headers = ServerConstants.httpHeaders
return headers
}
}
Service
class TopUpService: TopUpServiceProtocol {
let provider = MoyaProvider<TopUpAPI>()
func topUp(request: TopUpWithAmountRequest, completion: #escaping (Result<Response, MoyaError>) -> Void) {
provider.request(.topUpWithAmount(request: request), completion: completion)
}
}
Caller code
let request = TopUpWithAmountRequest(amount: Int(amount), redirect: true)
view?.showProgressHUD()
topUpService.topUpWithRedirector(request: request) { [weak self] result in
switch result {
case .success(let response):
...
case .failure(let error):
print("--- Error in TopUpAmount: \(error)")
self?.view?.showErrorNotification()
...
}
self?.view?.hideProgressHUD()
}
I can't realise, how to modify redirect body with Moya. But I did it with Alamofire:
Alamofire solution:
func topUpWithRedirector(amount: Int, completion: #escaping (DataResponse<TopUpPaymentAFResponse, AFError>) -> Void) {
let newApi = "https://00.00.00.00/api/commitPayment"
let request = AF.request(
newApi,
method: HTTPMethod.post,
parameters: ["Amount" : amount, "ShouldRedirect" : true],
encoding: JSONEncoding.default,
headers: [ "Content-Type" : "application/json", "MobileReq" : "ios" ]
)
let redirector = Redirector(behavior: .modify({ task, request, responce in
var newRequest: URLRequest = request
let parameters: [String: Any] = [
"Amount" : amount
]
let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: [])
newRequest.httpBody = httpBody
return newRequest
}))
request.redirect(using: redirector).responseDecodable(of: TopUpPaymentAFResponse.self, completionHandler: completion)
}
So, I need help to realise this with Moya library. Thanks.

Swift form-data using Alamofire 5 with parameters

I am beginner iOS developer and I don't know how to upload a file using Alamofire 5, I know there are a lot of similar questions, but all the topics here are from very old versions and I couldn't get it working. I tried this code, but I couldn't fit to my case, it gives me success, but file is not uploaded and the result I get is not what I get in postman. This is the code:
func uploadFile(_ parameters: Parameters) {
AF.upload(multipartFormData: { multipartFormData in
URLEncoding.default.queryParameters(parameters).forEach { (key, value) in
if let data = value.data(using: .utf8) {
multipartFormData.append(data, withName: key)
}
}
}, to: url)
.responseDecodable(of: FileCreation.self) { response in
switch response.result {
case .success(let data):
print(data, "success")
case .failure(let error):
print(error)
}
}
}
usage:
#IBAction func upload(_ sender: UIButton) {
guard let data = image?.jpegData(compressionQuality: 0.5)! else { return }
let parameters = ["addFiles": data]
uploadFile(parameters)
}
Here's Xcode output:
Here you can see postman response after I upload file:
Alamofire.upload(multipartFormData: {
multipartFormData in
if let imageData = image[0].jpegData(compressionQuality: 0.6) {
multipartFormData.append(imageData, withName: "addFiles", fileName: "file.pdf", mimeType: "application/pdf")
}
for (key, value) in param {
multipartFormData.append((value as AnyObject).data(using: String.Encoding.utf8.rawValue)!, withName: key)
}
},to: apiurl, method: .post, headers: headers, encodingCompletion: { encodingResult in
switch encodingResult {
case .success(let upload, _, _):
upload.responseJSON {
response in
print(response.result)
}
//break
case .failure(let encodingError):
break
}
})
Try This
func uploadFilesToServer(_ url: String, method: HTTPMethod, parameters: [String:Any]?, file: [String:Any]?, fileType: String, fileName: String, headers:HTTPHeaders?, completionHandler: #escaping (_ result: Data?, _ success: Bool, _ status: String) -> ()) {
var status = Bool()
var message = String()
let url = URL(string: url)
AF.upload(multipartFormData: { multiPart in
if let files = file {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy-hh-mm-ss"
let dateString = dateFormatter.string(from: Date())
for (key, value) in files {
if fileType == "pdf" {
let filePath = URL(fileURLWithPath: value as! String)
multiPart.append(filePath, withName: "\(key)", fileName: fileName, mimeType: "file/pdf")
} else {
multiPart.append(value as! Data, withName: "\(key)", fileName: "Uploads\(dateString).png", mimeType: "image/png")
}
}
}
if let params = parameters {
for (key, value) in params {
multiPart.append((value as AnyObject).data(using: String.Encoding.utf8.rawValue)!, withName: key)
}
}
}, to: url!, method: method, headers: headers ?? nil)
.validate()
.uploadProgress(closure: { progress in
print(progress)
})
.responseJSON { response in
switch response.result {
case .success(let responseData):
print(responseData)
case .failure(let networkErr):
switch networkErr {
case .responseSerializationFailed(reason: _):
message = "Something went wrong"
case .sessionTaskFailed(error: let err):
message = err.localizedDescription
default:
message = "Something went wrong"
}
completionHandler(nil, false, message)
break
}
}
}
usage
uploadFilesToServer(url, method: .post, parameters: params, file: uploadFile, fileType: "pdf", fileName: fileNme, headers: tokenHeader) { [self] responseData, isSuccess, responseMsg in
if isSuccess {
} else {
}
}

Swift Combine Memory Leak from CFNetwork

I am trying to plug a memory leak. I have the following class that fetches API requests:
public struct Service {
public let baseURL: URL
public let session: URLSession
public init (baseURL: URL, session: URLSession) {
self.baseURL = baseURL
self.session = session
}
public struct Response {
public let data: Data
public let response: URLResponse
}
public enum ServiceError: Error {
case api(title: String, messages: [String])
case other(Error)
}
struct ServiceErrorResponse: Decodable {
let response: ErrorResponse
enum CodingKeys: String, CodingKey {
case response = "error"
}
}
struct ErrorResponse: Decodable {
let title: String
let messages: [String]
}
public enum HTTPMethod: String {
case get = "GET"
case put = "PUT"
case post = "POST"
case patch = "PATCH"
case delete = "DELETE"
}
public func run(_ request: URLRequest) -> AnyPublisher<Response, ServiceError> {
return session
.dataTaskPublisher(for: request)
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
let error = try JSONDecoder().decode(ServiceErrorResponse.self, from: data)
let title = error.response.title
let messages = error.response.messages
print(error.response)
throw ServiceError.api(title: title, messages: messages)
}
return Response(data: data, response: response)
}
.mapError { err in
let error = err is ServiceError ? err : ServiceError.other(err)
return error as! Service.ServiceError
}
.eraseToAnyPublisher()
}
public func fetch(
_ path: String,
method: HTTPMethod = .get,
params: Data? = nil
) -> AnyPublisher<Response, ServiceError> {
let url: URL
if let params = params, method == .get {
url = buildGetURLWithParams(path: path, params: params)!
}
else {
url = baseURL.appendingPathComponent(path)
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
if let params = params, method != .get {
request.httpBody = params
}
return run(request)
}
private func buildGetURLWithParams(path: String, params: Data) -> URL? {
if let json = try? JSONSerialization.jsonObject(with: params, options: []) as? [String: String] {
var urlComponents = URLComponents(
url: baseURL.appendingPathComponent(path),
resolvingAgainstBaseURL: false
)
urlComponents?.queryItems = json.map { URLQueryItem(name: $0, value: $1) }
return urlComponents?.url
}
else { return nil }
}
}
I then make requests from the app using the following:
typealias ServiceResponse = Service.Response
typealias ServiceError = Service.ServiceError
typealias ServiceMethod = Service.HTTPMethod
enum MyAPI {
static let service = Service(
baseURL: URL(string: "http://127.0.0.1:3000/api")!,
session: URLSession(configuration: URLSessionConfiguration.default)
)
static func login(email: String, password: String) -> AnyPublisher<ServiceResponse, ServiceError> {
let params = ["email": email, "password": password]
let json = try! JSONEncoder().encode(params)
return service.fetch("/login", method: .post, params: json)
}
}
The login function fetches a response and returns an AnyPublisher which is consumed as follows:
enum UserAction {
case login
case loginSuccess(UserResponse)
case loginFailure
case logout
static func login(email: String, password: String) -> Dispatch<AppAction> {
return { dispatch in
dispatch(.userAction(action: .login))
return MyAPI.login(email: email, password: password)
.map(\.data)
.decode(type: UserResponse.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case .failure(let err) = completion {
print("--------------------------")
print("Retrieving data failed with error \(err)")
}
},
receiveValue: { result in
dispatch(.userAction(action: .loginSuccess(result))) // Here I have a memory leak
}
)
}
}
}
I am mimicking something like Redux where a dispatch actions to change state, so I return an 'effect' from the UserAction login that gets the dispatch function. Everything works nicely but at the receiveValue line I get a memory leak with the following description:
Any ideas what could be causing it or how I can find out? Im fairly new to Xcode and Swift.

How can I use Alamofire Router to organize the API call? [swift/ Alamofire5]

I'm trying to convert my AF request to Router structures for a cleaner project. I'm getting an error for:
Value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols.
Please help me to fix the code. THANK YOU!
My URL will have a placeholder for username and the password will be sent in body. The response will be Bool (success), username and bearer token.
Under is my AF request:
let username = usernameTextField.text
let password = passwordTextField.text
let loginParams = ["password":"\(password)"]
AF.request("https://example.com/users/\(username)/login",
method: .post,
parameters: loginParams,
encoder: JSONParameterEncoder.default,
headers: nil, interceptor: nil).response { response in
switch response.result {
case .success:
if let data = response.data {
do {
let userLogin = try JSONDecoder().decode(UsersLogin.self, from: data)
if userLogin.success == true {
defaults.set(username, forKey: "username")
defaults.set(password, forKey: "password")
defaults.set(userLogin.token, forKey: "token")
print("Successfully get token.")
} else {
//show alert
print("Failed to get token with incorrect login info.")
}
} catch {
print("Error: \(error)")
}
}
case .failure(let error):
//show alert
print("Failed to get token.")
print(error.errorDescription as Any)
}
}
What I have so far for converting to AF Router structures:
import Foundation
import Alamofire
enum Router: URLRequestConvertible {
case login(username: String, password: String)
var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
var path: String {
switch self {
case .login(let username):
return "/users/\(username)/login"
}
}
var parameters: Parameters? {
switch self {
case .login(let password):
return ["password": password]
}
}
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try Constants.ProductionServer.baseURL.asURL()
var request = URLRequest(url: url.appendingPathComponent(path))
// HTTP Method
request.httpMethod = method.rawValue
// Common Headers
request.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
request.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
// Parameters
switch self {
case .login(let password):
request = try JSONParameterEncoder().encode(parameters, into: request) //where I got the error
}
return request
}
}
class APIClient {
static func login(password: String, username: String, completion: #escaping (Result<UsersLogin, AFError>) -> Void) {
AF.request(Router.login(username: username, password: password)).responseDecodable { (response: DataResponse<UsersLogin, AFError>) in
completion(response.result)
}
}
}
LoginViewController Class (where I replaced the AF.request code)
APIClient.login(password: password, username: username) { result in
switch result {
case .success(let user):
print(user)
case .failure(let error):
print(error.localizedDescription)
}
Codable UsersLogin model
struct UsersLogin: Codable {
let success: Bool
let username: String
let token: String?
enum CodingKeys: String, CodingKey {
case success = "success"
case username = "username"
case token = "token"
}
}
Took me a while but finally fixed it. I also clean up the code too.
enum Router: URLRequestConvertible {
case login([String: String], String)
var baseURL: URL {
return URL(string: "https://example.com")!
}
var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
var path: String {
switch self {
case .login(_, let username):
return "/users/\(username)/login"
}
}
func asURLRequest() throws -> URLRequest {
print(path)
let urlString = baseURL.appendingPathComponent(path).absoluteString.removingPercentEncoding!
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.method = method
switch self {
case let .login(parameters, _):
request = try JSONParameterEncoder().encode(parameters, into: request)
}
return request
}
}
Usage
let username = usernameTextField.text
AF.request(Router.login(["password": password], username)).responseDecodable(of: UsersLogin.self) { (response) in
if let userLogin = response.value {
switch userLogin.success {
case true:
print("Successfully get token.")
case false:
print("Failed to get token with incorrect login info.")
}
} else {
print("Failed to get token.")
}
}
I solved a similar problem in this way. I created a protocol Routable
enum EncodeMode {
case encoding(parameterEncoding: ParameterEncoding, parameters: Parameters?)
case encoder(parameterEncoder: ParameterEncoder, parameter: Encodable)
}
protocol Routeable: URLRequestConvertible {
var baseURL: URL { get }
var path: String { get }
var method: HTTPMethod { get }
var encodeMode: EncodeMode { get }
}
extension Routeable {
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var urlRequest: URLRequest
switch encodeMode {
case .encoding(let parameterEncoding, let parameters):
urlRequest = try parameterEncoding.encode(URLRequest(url: url), with: parameters)
case .encoder(let parameterEncoder, let parameter):
urlRequest = URLRequest(url: url)
urlRequest = try parameterEncoder.encode(AnyEncodable(parameter), into: urlRequest)
}
urlRequest.method = method
return urlRequest
}
}
And my routers look like this one
enum WifiInterfacesRouter: Routeable {
case listActive(installationId: Int16?)
case insert(interface: WifiInterface)
var encodeMode: EncodeMode {
switch self {
case .listActive(let installationId):
guard let installationId = installationId else {
return .encoding(parameterEncoding: URLEncoding.default, parameters: nil)
}
return .encoding(parameterEncoding: URLEncoding.default, parameters: ["idInstallation": installationId])
case .insert(let interface):
return .encoder(parameterEncoder: JSONParameterEncoder.default, parameter: interface)
}
}
var baseURL: URL {
return URL(string: "www.example.com/wifiInterfaces")!
}
var method: HTTPMethod {
switch self {
case .listActive: return .get
case .insert: return .post
}
}
var path: String {
switch self {
case .listActive: return "listActive"
case .insert: return "manage"
}
}
}
To solve the build error
Protocol 'Encodable' as a type cannot conform to the protocol itself
I used the useful AnyCodable library. A type erased implementation of Codable.
You can't use Parameters dictionaries with Encodable types, as a dictionary of [String: Encodable] is not Encodable, like the error says. I suggest moving that step of the asURLRequest process into a separate function, such as:
func encodeParameters(into request: inout URLRequest) {
switch self {
case let .login(parameters):
request = try JSONParameterEncoder().encode(parameters, into: request)
}
}
Unfortunately this doesn't scale that well for routers with many routes, so I usually break up my routes into small enums and move my parameters into separate types which are combined with the router to produce the URLRequest.

How to make put request parameters with image Alamofire?

I am sending request through this format to make request with parameters. But how will I sent image as multipart body with this parameters?
class APIManager: NSObject {
enum Router: URLRequestConvertible {
static var baseURLString = baseURLString
static let xSource = X_SOURCE_HEADER
case updateProfile([String: AnyObject])
var method: HTTPMethod {
switch self {
case .updateProfile:
return .put
}
}
var path: String {
switch self {
case .updateStockShelfUnits:
return profile_url
}
}
func asURLRequest() throws -> URLRequest {
let url = try GlobalData.gBaseURLString.asURL()
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue
switch self {
case .updateProfile(let parameters):
urlRequest.setValue(access_token, forHTTPHeaderField: "X-User-Token")
urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
}
return urlRequest
}
}
func makeRequestToUpdateProfile(param: [String : AnyObject], img: UIImage, completion: #escaping completionHandlerWithSuccessAndErrorMessage) {
Alamofire.request(Router.updateprofile(param)) .responseJSON { response in
switch response.result {
case .success(let JSON):
print(JSON)
completion(true, "Success")
case .failure(let Error):
print("Request failed with error: \(Error)")
completion(false, Error.localizedDescription)
}
}
}
}
Here what I will do to make a request parameters with image as multipart body? Requesting api with only parameters working well.
Upload Photo / File with parameters and custom headers via Swift 3 and Alamofire 4
// import Alamofire
func uploadWithAlamofire() {
let image = UIImage(named: "bodrum")!
// define parameters
let parameters = [
"hometown": "yalikavak",
"living": "istanbul"
]
Alamofire.upload(multipartFormData: { multipartFormData in
if let imageData = UIImageJPEGRepresentation(image, 1) {
multipartFormData.append(imageData, withName: "file", fileName: "file.png", mimeType: "image/png")
}
for (key, value) in parameters {
multipartFormData.append((value?.data(using: .utf8))!, withName: key)
}}, to: "upload_url", method: .post, headers: ["Authorization": "auth_token"],
encodingCompletion: { encodingResult in
switch encodingResult {
case .success(let upload, _, _):
upload.response { [weak self] response in
guard let strongSelf = self else {
return
}
debugPrint(response)
}
case .failure(let encodingError):
print("error:\(encodingError)")
}
})
}
Credit: fatihyildizhan