Please help and explain why the router doesn't have responseDecodable. I made a router for AF request call and one of the endpoint need to send up String: [String: Any]. I'm not sure what I did wrong. Thank you!
AFRouter
enum AFRouter: URLRequestConvertible {
case test([String: [String: Any]])
var base: URL {
return URL(string: "https://example.com")!
}
var method: HTTPMethod {
switch self {
case .test:
return .get
}
var path: String {
switch self {
case .test(_):
return "/v2/test"
}
}
func asURLRequest() throws -> URLRequest {
let urlString = baseURL.appendingPathComponent(path).absoluteString.removingPercentEncoding!
let removeSpace = urlString.replacingOccurrences(of: " ", with: "")
let url = URL(string: removeSpace)
var request = URLRequest(url: url!)
request.method = method
switch self {
case .test(_):
guard let token = defaults.string(forKey: "token") else {
return request
}
request.setValue("Bearer " + token , forHTTPHeaderField: "Authorization")
request = try JSONEncoding.default.encode(request)
return request
}
}
Codable
struct Test: Codable {
let success: String
let message: String
let data: [String]
}
Calling API
func getTest(testInfo: [String: Any]) {
AF.request(AFRouter.test(["Testing": testInfo]).responseDecodable(of: Test.self) { response in //got error here "Value of type 'AFRouter' has no member 'responseDecodable'"
//do something...
})
}
The error is saying that you want to use responseDecodable(of:) on a AFRouter instance.
But, in fact, you want to use it on a DataRequest instance.
But it "should work", so are you calling it on a mistaken instance? if we observe, there is a missing ):
AF.request(AFRouter.test(["Testing": testInfo]).responseDecodable(of:...
=>
AF.request(AFRouter.test(["Testing": testInfo])).responseDecodable(of:...
Related
I created a reusable Alamofire request which works smoothly. I am trying to get the data from the request Decode it(works fine too) and append it to one of my arrays to display in tableView/collectionView.
I am using MVVM and I append my data in viewModel(you can see below). The thing is I have tableView in my viewController and inside my tableView methods( viewForSupplementaryElementOfKind for instance) the 'stories'(from viewModel) are always empty.
In my viewDidLoad method I call getMainPageData(from viewModel) first and then create my tableView. I assure you the request itself is a success, the only problem is displaying the data.
Please keep in mind that the project has many API calls so I need a solution which will work in all cases when I have to deal with "lists". Thank you in advance
class NetworkManager {
let keychain = KeychainManager()
let base = "SomeBase"
let storageManager = StorageManager()
func setupRequest(path: Paths, method: RequestMethod, body: Encodable? = nil, params: [String: Any]? = nil, header: HeaderType, completion: #escaping((Result<Data,NetworkError>) -> Void)) {
var queries = ""
if let params = params {
queries = params.passingQuery()
}
let url = URL(string: base + path.rawValue + queries)
var request = URLRequest(url: url!)
request.httpMethod = method.rawValue
request.setValue(header.value[0].value, forHTTPHeaderField: "Content-Type")
if let userToken = keychain.getAccessToken(), userToken.count > 0 {
request.setValue("Bearer " + userToken, forHTTPHeaderField: "Authorization")
}
if let body = body {
if let jsonData = body.toJSONData() {
request.httpBody = jsonData
}
}
AF.request(request).validate().responseJSON { response in
if (200...299) ~= response.response?.statusCode ?? -1 {
self.handlingHeaders(response: response)
completion(.success(response.data!))
} else {
do {
if let data = response.data {
let json = try JSONDecoder().decode(ErrorResponse.self, from: data)
completion(.failure(.responseError(json.message)))
}
} catch {
completion(.failure(.serverError))
}
}
}
}
private func handlingHeaders(response: AFDataResponse<Any>) {
let headers = response.response?.headers
if let accessToken = headers?.dictionary["Authorization"] {
keychain.saveToken(token: accessToken)
}
}
}
extension Encodable {
func toJSONData() -> Data? { try? JSONEncoder().encode(self) }
}
var stories = [Story]()
func getMainPageData(completion: #escaping(Result<Void, NetworkError>) -> ()) {
networkManager.setupRequest(path: .mainPageData, method: .get, body: nil, params: nil, header: .application_json) { [self] result in
switch result {
case .success(let data):
do {
let homePageData = try JSONDecoder().decode(MainPageResponse.self, from: data)
stories.append(contentsOf: homePageData.model.stories)
I'm totally new to swift and iOS programming so I'm a little lost on how to do this and even in what files I should be doing this too.
I'm trying to do a http post request to get calendar events and save them in the app to later use and display.
I made a model class with this code.
import UIKit
class Event {
var id: Int
var init_date: String
var end_date: String
var title: String
var description: String
var color_code: String
var all_day: Int
init?(id: Int, init_date: String, end_date: String, title: String, description: String, color_code: String, all_day: Int) {
//Initialization should fail if these are false
if id < 0 || init_date.isEmpty || end_date.isEmpty || title.isEmpty {
return nil
}
//Initialize stored properties
self.id = id
self.init_date = init_date
self.end_date = end_date
self.title = title
self.description = description
self.color_code = color_code
self.all_day = all_day
}
}
But now I don't know what the next step would be. I need this to be downloaded immediately once the app is opened for the first time and not when it's not being opened for the first time. Do I create a new method in the ViewController.swift for the download?
Right now I haven't added anything to the ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
What should I do next?
At this point you need to create a function that handles the POST request you are making.
Once completed, place this function inside your appDelegate main function didFinishLaunchingWithOptions. This is the function that executes on appStart
On a successful function call save the data (presumably json) into a Global Variable or whatever you need for you app.
TIP:
On you class
class Event: Codable {
}
make sure to add Codable like above
Below is an example of what your post request will look like
func myPostRequest(completionHandler: #escaping (Bool?, String?) -> Void){
guard let url = URL(string:"") else { return }
let parameters = ["": ""]
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil
else {
print(error as Any)
return
}
if let httpResponse = response as? HTTPURLResponse {
if (httpResponse.statusCode == 200) {
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]
//print("^^^^^^^^^^^^^^",json)
for x in json ?? [] {
//here is where you will parse your data from the post request
}
completionHandler(true, nil)
return
}
} else {
completionHandler(false, "No Response From Server")
print("Failure response: STATUS CODE != 200")
}
} else {
completionHandler(false, "Database Connection Error")
print("Error \(error!)")
}
}
task.resume()
} catch let error {
completionHandler(false, "failure")
print("POSTERROR: \(error.localizedDescription)")
}
}
I use Alamofire, you can add it to your project via:
Pods
Swift Package Manager
When you add the framework you can use it:
import Alamofire
Then you need to make your class with the protocol Codable to pass the data to your class.
class Event: Codable { }
Then you need to call the url and store the response in a variable:
override func viewDidAppear(_ animated: Bool) {
AF.request("your API rest url").responseData { (resData) in
guard let data = resData.data else { return }//Check if the data is valid
do {
let decoder = JSONDecoder()//Initialize a Json decoder variable
let decodedData = try decoder.decode(Event.self, from: data)//Decode the response data to your decodable class
//Print the values
print(decodedData.headers)
print(decodedData.id)
print(decodedData.init_date)
print(decodedData.end_date)
} catch {
print(error.localizedDescription)
}
}
}
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.
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.
So I'm writing my networking code using a router design pattern. I'm writing a new router for different components of my app (should i be doing this? I try to limit my objects lines of code). Heres my router enum. If I was using a class, I could define a method once to populate variables like HTTPMethod and override them if necessary. Is there a way to do this with enums? Is it worth implementing or should i repeat the same code. There are a few other places besides httpMethod such as URL construction where I think this could be helpful.
I was thinking i could do something with protocols but am not sure if I'm wasting my time.
enum PRRouter: URLRequestConvertible {
static let baseURLString = "http://localhost:8000/"
case get(Int)
case create([String : Any])
case delete(Int)
func asURLRequest() throws -> URLRequest {
var method: HTTPMethod{
switch self {
case .get:
return .get
case .create:
return .post
case.delete:
return .delete
}
}
let params : ([String : Any]?) = {
switch self {
case .get, .delete:
return nil
case .create(let newTodo):
return newTodo
}
}()
let url : URL = {
let relativePath: String?
switch self{
case .get(let number):
relativePath = "test/\(number)"
case .create:
relativePath = "test/"
case .delete:
relativePath = "test/"
}
var url = URL(string: PRRouter.baseURLString)!
if let relativePath = relativePath {
url = url.appendingPathComponent(relativePath)
}
return url
}()
var urlRequest = URLRequest(url:url)
urlRequest.httpMethod = method.rawValue
let encoding = JSONEncoding.default
return try encoding.encode(urlRequest, with: params)
}
Make the enum conform to a protocol with a default implementation.
protocol P {
func f()
}
extension P {
func f() { print("default implementation") }
}
enum E: P {
case Foo
}
let e = E.Foo
e.f()
I do something similar in my own project. Here is an example based on your code to get you started:
protocol APIProtocol {
var path: String { get }
var method: HTTPMethods { get }
var bodyParameters: [String: Any?]? { get }
}
enum HTTPMethods: String {
case get = "GET"
case post = "POST"
}
enum PRRouter: APIProtocol {
case get(Int)
case create([String : Any])
case delete(Int)
var path: String {
switch self {
case let .get(number):
return "test/\(number)"
default:
return "test"
}
}
var method: HTTPMethods {
return .get
}
var bodyParameters: [String : Any?]? {
return nil
}
}
extension APIProtocol {
func execute(completion: #escaping ((Data?) -> Void)) -> URLSessionDataTask? {
guard let url = URL(string: "http://localhost:8000/\(path)") else { return nil }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
if let bodyParameters = bodyParameters {
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: bodyParameters, options: [.prettyPrinted])
}
let task = URLSession.shared.dataTask(with: urlRequest) { (data, urlResponse, error) in
completion(data)
}
task.resume()
return task
}
}
Finally you can use it like this:
let dataTask = PRRouter.get(2).execute { (data) in
//
}
You could extend this further by changing the completion block in the execute function to return a deserialized object.