I am trying to make a post request with Alamofire 5. I have to use Dictionary<String, Any> for parameters. Because I am writing a wrapper for Alamofire. But it seems i can't be able to use Any object in a dictionary because Alamofire gives me a compiler error:
Value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols
What i've tried:
let encodedParameters = Dictionary<String, Any>
AF.request(url, method: .get, parameters: encodedParameters, headers: headers)
Some values will be string others will be integer in my dictionary. So i can't use constant type. How can i resolve this problem?
It's because you are using the newer method which requires the parameters parameter to be Encodable.Use the older Alamofire method and you will be fine:
AF.request(url, method: .get, parameters: encodedParameters, encoding: JSONEncoding.default, headers: headers)
Update: If you want to use the latest Alamofire 5 syntax create a struct and confirm it to encodable. Then create an object of the same struct with values and pass it.
To use the new request, you can create your own structs for your request parameters:
// you might have...
struct FooRequestParameters : Codable {
let paramName1: Int
let paramName2: String
}
// for another type of request, you might have different parameters...
struct BarRequestParameters: Codable {
let somethingElse: Bool
}
And you can pass a FooRequestParameters(paramName1: 1, paramName1: "hello") instead of your dictionary. This would be the same as passing the dictionary:
[
"paramName1": 1,
"paramName2": "hello"
]
The rationale behind this API change is likely to have more safety. With a [String: Any], you could very easily, say, give a String value to a parameter that is supposed to be Int, or type a parameter's name wrongly, or missed some parameters without realising... etc etc.
If you want pass parameters of [String: Any] you don't need to create structs and adopt the encodable protocol. This can be achieved like this:
import Foundation
import Alamofire
enum ServiceRouter {
case fetchCountries
case fetchUserRepositories
var baseURL: String {
switch self {
case .fetchCountries:
return "https://restcountries.eu/rest/v2"
}
}
var path: String {
switch self {
case .fetchCountries:
return "/all"
}
}
var method: HTTPMethod {
switch self {
case .fetchCountries:
return .get
default:
return .post
}
}
var parameters: Parameters? {
switch self {
case .fetchUserRepositories:
return ["per_page": 100]
default:
return [:]
}
}
}
// MARK: - URLRequestConvertible
extension ServiceRouter: URLRequestConvertible {
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL().appendingPathComponent(path)
var request = URLRequest(url: url)
request.method = method
if method == .get {
request = try URLEncoding.default.encode(request, with: parameters)
} else if method == .post {
request = try JSONEncoding.default.encode(request, with: parameters)
}
return request
}
}
And you can use this request as
AF.request(ServiceRouter.fetchCountries).responseData { (response) in
With the Alamofire 5 you can use this with Any type
AF.request("{url}", method: .post, parameters: params, encoding: JSONEncoding.prettyPrinted).response { response in
print(response)
}
I hope this helps someone
Related
I have a struct like this
struct ApiResponse<T: Codable>: Codable {
let result: T?
let statusCode: String?
}
Somewhere in my code I need statusCode. I am not interested in result but Swift is not allowing me to use following:
let apiResponse = value as? ApiResponse
it shows following error:
Generic parameter 'T' could not be inferred in cast to 'ApiResponse'
which is quite obvious since struct definition ask some struct conforming to Codable but at same time i can not use one type as it will fail for other types.
e.g.
let apiResponse = value as? ApiResponse<ApiResult>
would be true for one type of response but if i have ApiResponse<ApiOtherResult> it will fail.
NetworkLayer.requestObject(router: router) { (result: NetworkResult<T>) in
switch result {
case .success(let value):
if let apiResponse = value as? ApiResponse {
}
case .failure: break
}
}
I'd suggest adding a new protocol
protocol StatusCodeProvider {
var statusCode: String? { get }
}
Add it as a requirement in your function making sure that T in NetworkResult<T> conforms to StatusCodeProvider and add conformance for every T you want to request.
In my application, several controllers have a very similar code structure, the differences are minimal, so for optimization I decided to create a basis for these controllers, and inherit each specific controller from the basis.
I have a function for sending network requests and processing a response, I pass the response structure as a parameter to this function, so that the function returns a ready-made response structure to me. Each such structure is Decodable.
An example of such a structure:
struct APIAnswerUserActivity: Decodable {
let status: String
let code: Int?
let data: [UserActivity]?
let total: Int?
}
Function for network requests, an object (structure) of the Decodable.Protocol type is accepted as a jsonType parameter:
public func networkRequest<T: Decodable> (
url: String,
timeout: Double = 30,
method: URLMethods = .GET,
data: [String : String]? = nil,
files: [URL]? = nil,
jsonType: T.Type,
success: #escaping (T) -> Void,
failure: #escaping (APIError) -> Void
) -> URLSessionDataTask { ... }
There are several parameters in the main controller that I override through override in the child controllers, one of these parameters should be an object of type Decodable for the general function to receive data correctly. The JSON structures of the response are very similar, but still slightly different, a common structure for them cannot be created, because the data is still a little different.
If in the main controller do this:
public var decodableType: Decodable.Type {
return APIAnswerUserActivity.self
}
That will work, and it is possible to redefine types, but the network function does not accept this, it needs the Decodable.Protocol object. If the type decodable.Protocol is specified for the variable decodableType, then it is no longer possible to add APIAnswerUserActivity.self, which is quietly accepted when the networkRequest function is called.
How to be in this situation? I hope that I managed to correctly and clearly state the essence of my problem. Thanks!
#Владислав Артемьев, I'm still not sure that I completely understand the problem because you haven't shared the code that takes the Decodable class. But the issues seems to be about how to pass a Decodable class.
I hope the following can help clarify how you can impose the right constraint on the generic and how you should declare the variable. You can paste it into a playground and experiment.
import Foundation
struct FakeToDo: Decodable {
var userId: Int
var id: Int
var title: String
var completed: Bool
}
enum URLMethods {
case GET
case POST
}
func networkRequest<T: Decodable> (
url: String,
timeout: Double = 30,
method: URLMethods = .GET,
data: [String : String]? = nil,
files: [URL]? = nil,
jsonType: T.Type,
success: #escaping (T) -> Void,
failure: #escaping (Error) -> Void
) -> URLSessionDataTask {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { (data, response, error) in
if error != nil {
failure(error!)
return
}
guard let data = data else { return }
let decoder = JSONDecoder()
guard let value = try? decoder.decode(T.self, from: data) else { return }
// get back on the main queue for UI
DispatchQueue.main.async {
success(value)
}
})
return task
}
class Example<T> where T: Decodable {
let type: T.Type
init(_ type: T.Type) {
self.type = type
}
public var decodableType: T.Type {
return type
}
}
let decodableType = Example(FakeToDo.self).decodableType
let url = "https://jsonplaceholder.typicode.com/todos/1"
let task = networkRequest(url: url, jsonType: decodableType,
success: { value in print(value) },
failure: { error in print(error) })
task.resume()
I need to return a value from a function that has a closure in it.
I researched about returning value from closures, and found out that I should use 'completion handler' to get the result I want.
I saw posts here and articles explaining it but could not apply because I didn't find anything that matches with my problem.
class ViewController: UIViewController {
let urls = URLs()
override func viewDidLoad() {
super.viewDidLoad()
var leagueId = getLeagueId(country: "brazil", season: "2019")
print(leagueId) //PRINTING 0
}
func getLeagueId (country: String, season: String) -> Int {
let headers = Headers().getHeaders()
var leagueId = 0
let url = urls.getLeagueUrlByCountryAndSeason(country: country, season: season)
Alamofire.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON {
response in
if response.result.isSuccess {
let leagueJSON: JSON = JSON(response.result.value!)
leagueId = (leagueJSON["api"]["leagues"][0]["league_id"].intValue)
}
else {
print("error")
}
}
return leagueId
}
}
The value returned is always 0 because the closure value is not passing to the function itself.
Thanks a lot
So the reason why you're having this issue is because AlamoFire.request is asynchronous. There's a great explanation of asynchronous vs synchronous here But basically when you execute something asynchronously, the compiler does not wait for the task to complete before continuing to the next task, but instead will execute the next task immediately.
So in your case, the AlamoFire.request is executed, and while it's running, the next line after the block is immediately run which is the line to return leagueId which will obviously still be equal to zero since the AlamoFire.request task (function) has not yet finished.
This is why you need to use a closure. A closure will allow you to return the value, after AlamoFire.request (or any other asynchronous task for that matter) has finished running. Manav's answer above shows you the proper way to do this in Swift. I just thought I'd help you understand why this is necessary.
Hope this helps somewhat!
Edit:
Manav's answer above is actually partially correct. Here's how you make it so you can reuse that value the proper way.
var myLeagueId = 0;
getLeagueId(country: "brazil", season: "2019",success: { (leagueId) in
// leagueId is the value returned from the closure
myLeagueId = leagueId
print(myLeagueId)
})
The code below will not work because it's setting myLeagueId to the return value of getLeagueId and getLeagueId doesn't have a return value so it won't even compile.
myLeagueId = getLeagueId(country: "brazil", season: "2019",success: { (leagueId) in
print(leagueId)
})
You need to either return value from the function.
func getLeagueId (country: String, season: String)->Int
Else you need to use completion handlers.
func getLeagueId (country: String, season: String,success:#escaping (leagueId: Int) -> Void) {
let headers = Headers().getHeaders()
var leagueId = 0
let url = urls.getLeagueUrlByCountryAndSeason(country: country, season: season)
Alamofire.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON {
response in
if response.result.isSuccess {
let leagueJSON: JSON = JSON(response.result.value!)
leagueId = (leagueJSON["api"]["leagues"][0]["league_id"].intValue)
success(leagueId)
}
else {
print("error")
}
}
}
And then use it in your code :
getLeagueId(country: "brazil", season: "2019",success: { (leagueId) in
print(leagueId)
self.leagueId = leagueId
})
This is how you should implement completionBLock
func getLeagueId (country: String, season: String, completionBlock:((_ id: String, _ error: Error?) -> Void)?) {
let headers = Headers().getHeaders()
var leagueId = 0
let url = urls.getLeagueUrlByCountryAndSeason(country: country, season: season)
Alamofire.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON {
response in
if response.result.isSuccess {
let leagueJSON: JSON = JSON(response.result.value!)
if let leagueId = (leagueJSON["api"]["leagues"][0]["league_id"].intValue){
completionBlock?(leagueId,nil)
}else {
completionBlock?(nil,nil) // PASS YOUR ERROR
}
}
else {
completionBlock?(nil,nil) // PASS YOUR ERROR
}
}
}
func getLeagueId isn't return anything so you get 0, if you want to get the result from func getLeagueId you should add completion handler function that will update this value.
I try to call to Google Places Api using Moya and have a problem with URL. Maya change characters in my URL. In this case for example before character ? adds %3f and change , for %2C. When I copy and paste this address into my web browser, I receive an error, but when I delete %3f and change and %2C on , I receive a correct answer form API. What should I set in Moya if I don't want to change this characters in my url?
my Moya provider looks like that:
extension GooglePlacesService: TargetType {
var baseURL: URL {
return URL(string: "https://maps.googleapis.com")!
}
var path: String {
switch self {
case .gasStation:
return "/maps/api/place/nearbysearch/json?"
}
}
var parameters: [String : Any]? {
switch self {
case .gasStation(let lat, let long, let type):
return ["location": "\(lat),\(long)", "type" : "gas_station", "rankby" : "distance", "keyword" : "\(type)", "key" : GoogleKeys.googlePlacesKey]
}
}
var parameterEncoding: ParameterEncoding {
switch self {
case .gasStation:
return URLEncoding.queryString
}
}
var method: Moya.Method {
switch self {
case .gasStation:
return .get
}
}
var sampleData: Data {
switch self {
case .gasStation:
return "".utf8Encoded
}
}
var task: Task {
switch self {
case .gasStation:
return .request
}
}
}
private extension String {
var urlEscaped: String {
return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
}
var utf8Encoded: Data {
return self.data(using: .utf8)!
}
}
URL which generates Moya looks like that (doesn't work with API):
https://maps.googleapis.com/maps/api/place/nearbysearch/json%3F?key=MYAPIKEY&keyword=XXXXXX&location=51.0910166687869%2C17.0157277622482&rankby=distance&type=gas_station
URL which works with API:
https://maps.googleapis.com/maps/api/place/nearbysearch/json?key=MYAPIKEY&keyword=XXXXXX&location=51.0910166687869,17.0157277622482&rankby=distance&type=gas_station
I had the same issue with '?' gets converted to '%3F':
The solution is to leave the path without tricky symbols(like "?", ",") and to put them in var Task of Moya setup with URLEncoding.default instead:
How to add query parameter to URL in Moya
MOYA Convert or Replace %3f into ? mark
My URL with %3f:-
http://multiseller-dev.azurewebsites.net/api/Support/get-comments/34%3Fpage=1&pageSize=10?page=1&pageSize=10
IN Path
var path: String {
switch self {
case .GetComments(let id, let page, let pageSize):
return "api/Support/get-comments/\(id)"
}
}
IN TASK
var task: Task {
switch self {
case .GetComments(let id,let page,let pageSize):
let post = ["page" : page,
"pageSize" : pageSize
] as [String : Any]
return .requestParameters(parameters: post, encoding: URLEncoding.queryString)
}
}
OUTPUT URL:-
http://multiseller-dev.azurewebsites.net/api/Support/get-comments/34?page=1&pageSize=10?page=1&pageSize=10
You can write custom request mapping like:
final class func removePercentEncodingRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
do {
var urlRequest = try endpoint.urlRequest()
if let url = urlRequest.url,
let updatedString = url.absoluteString.removingPercentEncoding {
urlRequest.url = URL(string: updatedString)
}
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
and use it via MoyaProvider initializer:
MoyaProvider<YourProvider>(
requestClosure: MoyaProvider<YourProvider>.removePercentEncodingRequestMapping
)
The point is update urlRequest.url which is encoded wrong when you using "?", "," or another symbols in path
I'm working with Alamofire and SwiftyJSON. I want to build general request and parse model for common situations. Firstly, I make a protocol called JSONConvertible.
protocol JSONConvertible {
init?(json: JSON)
}
Secondly, I extend Request class in Alamofire.
extension Request {
func getResult(format: [String: AnyClass]) {
self.responseJSON { (response) in
guard let statusCode = response.response?.statusCode else {
return
}
switch statusCode {
case 200:
var result = [String: AnyObject]()
let json = JSON(rawValue: response.result.value!)!
for (key, className) in format {
if className.self is JSONConvertible {
let value = className.self(json: json[key]) // get error in this line
}
}
case 201..<400:
break
case 400...Int.max:
break
default:
break
}
}
}
}
But I get an error from the compiler. Because AnyObject is only protocol and it doesn't have this initializer. I don't want get a dictionary or array only. I want to get instances with concrete class. Please help me. Many thanks!
That's because inside the if the type for className.self is still AnyObject. You need to cast it to JSONConvertible and then can use the initializer.
if let concreteClass = className.self as? JSONConvertible.Type
{
let value = concreteClass.init(json: json[key])
}
If you are familiar with kotlin, swift doesn't do casting automatically when testing for type in an if clause.
I found another way to solve this. Define a new protocol.
protocol JSONConvertibleObject: AnyObject, JSONConvertible {
}
And use this instead.
extension Request {
func getResult(format: [String: JSONConvertibleObject]) {
self.responseJSON { (response) in
guard let statusCode = response.response?.statusCode else {
return
}
switch statusCode {
case 200:
var result = [String: AnyObject]()
let json = JSON(rawValue: response.result.value!)!
for (key, className) in format {
let value = className.self.dynamicType.init(json: json[key])
}
case 201..<400:
break
case 400...Int.max:
break
default:
break
}
}
}
}