I am creating a generic parser in my Network Layer with the help of associatedType and a Factory. Basic purpose is that I will call only one static function to whom I will pass type and data. It will do all the parsing stuff and will return me a Parsed Model Object.
protocol Parsable: Codable {
associatedtype JSON
static func Parse(object: Data) -> JSON?
}
Creation of Factory
struct ParseFactory<object: Parsable> {
let type: RequestType
func doParsing(data: Data) -> object.JSON? {
switch type {
case .RequestOne:
return ModelOne.Parse(object: data) as? object.JSON
case .RequestTwo:
return ModelTwo.Parse(object: data) as? object.JSON
}
}
}
Model Objects that create their own parsing stuff
class ModelOne: Parsable {
typealias JSON = ModelOne
let name: String
static func Parse(object: Data) -> JSON? {
let photoObject = try? JSONDecoder().decode(ModelOne.self, from: object)
return photoObject
}
}
class ModelTwo: Parsable {
typealias JSON = ModelTwo
let name: String
static func Parse(object: Data) -> JSON? {
let photoObject = try? JSONDecoder().decode(ModelTwo.self, from: object)
return photoObject
}
}
Call from Network Layer with single Line
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) -> Void in
// parsing
if let dataNotNil = data {
// _ = Parsable
}
}
Question: How can I call the one line factory function that will call the respective function of Model.
Note: Any Help would be much appreciated
Here how you can use
let session = URLSession.shared
let request = URLRequest(url: URL(string:"yoururl")!)
let task = session.dataTask(with: request) { (data, response, error) -> Void in
if let dataNotNil = data {
let objectParse = ParseFactory<ModelOne>(type: RequestType.RequestOne)
let modelOneObjc = objectParse.doParsing(data: dataNotNil)
print(modelOneObjc?.name)
}
}
Hope it is helpful
Related
I am wondering how I can potentially remove some duplicate methods in Swift 5.
func CreateProjectPossibleAccount(possibleAccount: ProjectPossibleAccount) -> Data {
var dataToReturn: Data?
self.dispatchGroup.enter()
do {
var request = getUrlRequestFor(RequestType.post)
request.httpBody = try JSONEncoder().encode(possibleAccount)
URLSession.shared.dataTask(with: request) {
(data, response, err) in
guard let data = data else {return}
dataToReturn = data
self.dispatchGroup.leave()
}.resume()
} catch {
print(error.localizedDescription)
}
self.dispatchGroup.wait()
return dataToReturn!
}
func CreateMaterialIssueItem(item: NewMaterialIssueItem) -> Data {
var dataToReturn: Data?
self.dispatchGroup.enter()
do {
var request = getUrlRequestFor(RequestType.post)
request.httpBody = try JSONEncoder().encode(item)
URLSession.shared.dataTask(with: request) {
(data, response, err) in
guard let data = data else {return}
dataToReturn = data
self.dispatchGroup.leave()
}.resume()
} catch {
print(error.localizedDescription)
}
self.dispatchGroup.wait()
return dataToReturn!
}
I have several functions like this where the only real difference is the custom type that I create. Is there a way to just name this function Post(item: T) and pass in all the unique types?
You can use generics to avoid code duplication. In the following example I'm assuming that ProjectPossibleAccount and NewMaterialIssueItem implement the Decodable protocol and also that RequestType is a enum. The dispatchGroup could also become a param of the method.
func request<T: Encodable>(item: T, requestType: RequestType) -> Data {
var dataToReturn: Data?
self.dispatchGroup.enter()
do {
var request = getUrlRequestFor(requestType)
request.httpBody = try JSONEncoder().encode(item)
URLSession.shared.dataTask(with: request) {
(data, response, err) in
guard let data = data else {return}
dataToReturn = data
self.dispatchGroup.leave()
}.resume()
} catch {
print(error.localizedDescription)
}
self.dispatchGroup.wait()
return dataToReturn!
}
and you can call the method like this:
let possibleAccount: ProjectPossibleAccount = ProjectPossibleAccount()
let data = request(item: possibleAccount, requestType: .post)
Another approach for the previous example ( and because it is a simple one) is simply specifying the item param data type as a Encodable, like this:
func request(item: Encodable, requestType: RequestType) -> Data
I am trying to make generic post method for API call.In my loadNew method I want to add normal dictionary inside resource object.Resource contains normal data which will pass from controller class.And dictionary is passed as body of request. but while encoding "Generic parameter 'T' could not be inferred" showing. How do I use dictionary in it?
struct Resource<T> {
let url: URL
let request: URLRequest
let dictionary : [String:Any]
let parse: (Data) -> T?
}
final class Webservice {
// MARK:- Generic
func load<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
URLSession.shared.dataTask(with: resource.url) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
func loadNew<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
do {
//FIXIT: error is getting here
let jsonBody = try JSONEncoder().encode(resource.dictionary)
request.httpBody = jsonBody
}catch{}
let session = URLSession.shared
session.dataTask(with: request) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
}
This method is called inside my Login controller.I have also tried assign it directly to request object but same error is showing
func APICall(){
guard let url = URL(string: Constants.HostName.local + Constants.API.User_Login) else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let resources = Resource<LoginReponse>(url: url, request: request, dictionary: dict){
data in
let loginModel = try? JSONDecoder().decode(LoginReponse.self, from: data)
return loginModel
}
// var response = LoginReponse()
Webservice().loadNew(resource: resources) {
result in
if let model = result {
print(model)
}
}
}
The error is a bit misleading, and may indicate you're using an older version of Xcode. In 11.4.1, the error is much more explicit:
error: value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols
The problem is that [String: Any] is not Encodable, because there's no way to encode "Any" (what should happen if you passed a UIViewController here? Or a CBPeripheral?)
Instead of a dictionary here, looking at your code I would expect you to pass an encodable object here. For example:
struct Resource<Value: Decodable, Parameters: Encodable> {
let url: URL
let request: URLRequest
let parameters : Parameters?
let parse: (Data) -> Value?
}
final class Webservice {
func loadNew<Value, Parameters>(resource: Resource<Value, Parameters>, completion: #escaping (Value?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
if let parameters = resource.parameters {
request.httpBody = try? JSONEncoder().encode(parameters)
}
// ...
}
That said, I'd probably turn this system around a bit. If you want to have a Request<T> (parameterized on the thing it returns, and not on the parameters it takes to generate it), that's fine. You can pack a bit more into the struct. For example:
let baseURL = URL(string: "https://example.com/api/")!
struct Resource<Value> {
let urlRequest: URLRequest
let parse: (Data) -> Result<Value, Error>
// Things you want as default for every request
static func makeStandardURLRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
return request
}
}
// It would be nice to have a default parser when you can, but you don't have to put that
// into Webservice. The Resource can handle it.
extension Resource where Value: Decodable {
init(urlRequest: URLRequest) {
self.init(urlRequest: urlRequest, parse: { data in
Result { try JSONDecoder().decode(Value.self, from: data) }
})
}
}
And then Resources are smart about themselves:
struct LoginParameters: Encodable {
let username: String
let password: String
}
struct LoginResult: Decodable {
let authToken: String
}
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent("login"))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
Of course that may get repeated a lot, so you can hoist it out:
extension Resource where Value: Decodable {
static func makeStandardURLRequest<Parameters>(endpoint: String, parameters: Parameters) -> URLRequest
where Parameters: Encodable {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent(endpoint))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
And then Login looks like:
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
return makeStandardURLRequest(endpoint: "login", parameters: parameters)
}
}
The point is that you can pull duplicated code into extensions; you don't need to stick it in the Webservice, or add more generic.
With that, your load gets a bit simpler and much more flexible. It focuses just on the networking part. That means that it's easier to swap out with something else (like something for unit tests) without having to mock out a bunch of functionality.
func load<Value>(request: Resource<Value>, completion: #escaping (Result<Value, Error>) -> ()) {
let session = URLSession.shared
session.dataTask(with: request.urlRequest) { data, response, error in
DispatchQueue.main.async {
if let data = data {
//completion call should happen in main thread
completion(request.parse(data))
} else if let error = error {
completion(.failure(error))
} else {
fatalError("This really should be impossible, but you can construct an 'unexpected error' here.")
}
}
}.resume()
}
There's a lots of ways to do this; for another, see this AltConf talk.
I have some basics in Swift, and I'm now trying to learn iOS development. I'm currently working in a small app that will ask resource on an API I've made that returns json made from :
struct A : Codable {
let name: String
let age: Int
}
struct B : Codable {
let something: String
}
Both API and app have these structs defined. As I'm always querying the same API, I thought of wrapping the part that ask the API some resources and decode this so I have an instance of the struct to use in my callback. Here's this method :
static func getContent(urlRequest: URLRequest, decodable: Decodable, completion: #escaping (Codable?, ErrorEnum?)->Void) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
data, response, error in
guard let data = data else {
completion(nil, .noData) // Handling errors in an enum
return
}
let decoder = JSONDecoder()
if let full = try? decoder.decode(decodable, from: data) {
completion(full, nil)
}
}
task.resume()
}
My problem concerns the decodable param. This shows an error and prevent me from compiling the app. After finding some resources on StackOverflow, I tried to change the parameters as
static func getContent(urlRequest: URLRequest, decodable: Decodable.Type, completion: #escaping (Codable?, ErrorEnum?)->Void)
I also tried to keep the parameter like this, and instead change inside the decode params
if let full = try? decoder.decode(decodable, from: data) {
completion(full, nil)
}
but nothing seems to satisfy the compiler... And looking at decode method inside Swift source code didn't help me that much as it requires T.Type where T is Decodable
My wish is to be able to use this as follow :
static func getA() {
guard let url = URL(string: "http://localhost/a") else { return }
let urlRequest = URLRequest(url: url)
getContent(urlRequest: urlRequest, decodable: A.self) {
a, error in
guard a = a else { return }
print(a.name!)
}
}
Do you have any idea how I could achieve this ? I also don't really know how to call this type of parameters or what to search on google that can lead me to the answer (lack of vocabulary).
Thank you !
try this just add a generic .Type of Codable and use its type as a parameter to pass foo.self
static func getContent<T: Codable>(urlRequest: URLRequest, decodable: T.Type, completion: #escaping (T?, ErrorEnum?)->Void) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
data, response, error in
guard let data = data else {
completion(nil, .noData) // Handling errors in an enum
return
}
let decoder = JSONDecoder()
if let full = try? decoder.decode(decodable, from: data) {
completion(full, nil)
}
}
task.resume()
}
You can use this:
func genericRequest<T: Decodable>(_ request: URLRequest, completion: #escaping APIGenericRequestCompletion<T>) {
Alamofire.request(request).responseData { (response) in
guard let data = response.data else {
completion(nil)
return
}
do {
let decodedObject = try JSONDecoder().decode(T.self, from: data)
completion(decodedObject)
} catch {
completion(nil)
}
}
}
where APIGenericRequestCompletion is:
typealias APIGenericRequestCompletion<T: Decodable> = (_ result: T?) -> Void
Then you use it as:
genericRequest(request) { (decodableObjectResponse) in
// your code here
}
class PostFOrData {
let url = NSURL( string: "http://210.61.209.194:8088/SmarttvWebServiceTopmsoApi/GetReadlist")
var picUrl = NSURL(string : "http://210.61.209.194:8088/SmarttvMedia/img/epi00001.png")
var responseString : NSString = ""
func forData() -> NSString {
let request = NSMutableURLRequest( URL: url!)
request.HTTPMethod = "POST"
var s : NSString = ""
let postString : String = "uid=59"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil {
println("error=\(error)")
return
} else {
println("response = \(response!)")
self.responseString = NSString(data: data, encoding: NSUTF8StringEncoding)!
println("responseString = \(self.responseString)")
}
}
// I want to return NSString here, but I always get nothing
return self.responseString
}
}
Anyone know how to get the data from task?
You can't return data directly from an asynchronous task.
The solution with Swift 2 is to make a completion handler like this:
class PostFOrData {
// the completion closure signature is (NSString) -> ()
func forData(completion: (NSString) -> ()) {
if let url = NSURL(string: "http://210.61.209.194:8088/SmarttvWebServiceTopmsoApi/GetReadlist") {
let request = NSMutableURLRequest( URL: url)
request.HTTPMethod = "POST"
let postString : String = "uid=59"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if let data = data,
jsonString = NSString(data: data, encoding: NSUTF8StringEncoding)
where error == nil {
completion(jsonString)
} else {
print("error=\(error!.localizedDescription)")
}
}
task.resume()
}
}
}
let pfd = PostFOrData()
// you call the method with a trailing closure
pfd.forData { jsonString in
// and here you get the "returned" value from the asynchronous task
print(jsonString)
}
That way, the completion is only called when the asynchronous task is completed. It is a way to "return" the data without actually using return.
Swift 3 version
class PostFOrData {
// the completion closure signature is (String) -> ()
func forData(completion: #escaping (String) -> ()) {
if let url = URL(string: "http://210.61.209.194:8088/SmarttvWebServiceTopmsoApi/GetReadlist") {
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postString : String = "uid=59"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request) {
data, response, error in
if let data = data, let jsonString = String(data: data, encoding: String.Encoding.utf8), error == nil {
completion(jsonString)
} else {
print("error=\(error!.localizedDescription)")
}
}
task.resume()
}
}
}
let pfd = PostFOrData()
// you call the method with a trailing closure
pfd.forData { jsonString in
// and here you get the "returned" value from the asynchronous task
print(jsonString)
}
There are some very generic requirements that would like every good API Manager to satisfy: will implement a protocol-oriented API Client.
APIClient Initial Interface
protocol APIClient {
func send(_ request: APIRequest,
completion: #escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Now Please check complete api structure
// ******* This is API Call Class *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void
/// Implementation of a generic-based API client
public class APIClient {
private let baseEndpointUrl = URL(string: "irl")!
private let session = URLSession(configuration: .default)
public init() {
}
/// Sends a request to servers, calling the completion method when finished
public func send<T: APIRequest>(_ request: T, completion: #escaping ResultCallback<DataContainer<T.Response>>) {
let endpoint = self.endpoint(for: request)
let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
if let data = data {
do {
// Decode the top level response, and look up the decoded response to see
// if it's a success or a failure
let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)
if let dataContainer = apiResponse.data {
completion(.success(dataContainer))
} else if let message = apiResponse.message {
completion(.failure(APIError.server(message: message)))
} else {
completion(.failure(APIError.decoding))
}
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
task.resume()
}
/// Encodes a URL based on the given request
/// Everything needed for a public request to api servers is encoded directly in this URL
private func endpoint<T: APIRequest>(for request: T) -> URL {
guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
fatalError("Bad resourceName: \(request.resourceName)")
}
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!
// Common query items needed for all api requests
let timestamp = "\(Date().timeIntervalSince1970)"
let hash = "\(timestamp)"
let commonQueryItems = [
URLQueryItem(name: "ts", value: timestamp),
URLQueryItem(name: "hash", value: hash),
URLQueryItem(name: "apikey", value: "")
]
// Custom query items needed for this specific request
let customQueryItems: [URLQueryItem]
do {
customQueryItems = try URLQueryItemEncoder.encode(request)
} catch {
fatalError("Wrong parameters: \(error)")
}
components.queryItems = commonQueryItems + customQueryItems
// Construct the final URL with all the previous data
return components.url!
}
}
// ****** API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
/// Response (will be wrapped with a DataContainer)
associatedtype Response: Decodable
/// Endpoint for this request (the last part of the URL)
var resourceName: String { get }
}
// ****** This Results type Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
public let offset: Int
public let limit: Int
public let total: Int
public let count: Int
public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
case encoding
case decoding
case server(message: String)
}
// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
/// Whether it was ok or not
public let status: String?
/// Message that usually gives more information about some error
public let message: String?
/// Requested data
public let data: DataContainer<Response>?
}
// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
let parametersData = try JSONEncoder().encode(encodable)
let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
return parameters.map { URLQueryItem(name: $0, value: $1.description) }
}
}
// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
case string(String)
case bool(Bool)
case int(Int)
case double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw APIError.decoding
}
}
var description: String {
switch self {
case .string(let string):
return string
case .bool(let bool):
return String(describing: bool)
case .int(let int):
return String(describing: int)
case .double(let double):
return String(describing: double)
}
}
}
/// **** This is your API Request Endpoint Method in Struct *****
public struct GetCharacters: APIRequest {
public typealias Response = [MyCharacter]
public var resourceName: String {
return "characters"
}
// Parameters
public let name: String?
public let nameStartsWith: String?
public let limit: Int?
public let offset: Int?
// Note that nil parameters will not be used
public init(name: String? = nil,
nameStartsWith: String? = nil,
limit: Int? = nil,
offset: Int? = nil) {
self.name = name
self.nameStartsWith = nameStartsWith
self.limit = limit
self.offset = offset
}
}
// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
public let id: Int
public let name: String?
public let description: String?
}
// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
let apiClient = APIClient()
// A simple request with no parameters
apiClient.send(GetCharacters()) { response in
response.map { dataContainer in
print(dataContainer.results)
}
}
}
I have some functions that are supposed to take data from the Wunderground API, and return a value from it. My class is below:
import UIKit
import Foundation
typealias ServiceResponse = (JSON, NSError?) -> Void
class APITest: NSObject {
static let sharedInstance = APITest()
let baseURL = "http://api.wunderground.com/api/91e65f0fbb35f122/history_20150811/q/OR/Portland.json"
func getRandomUser(onCompletion: (JSON) -> Void) {
let route = baseURL
makeHTTPGetRequest(route, onCompletion: { json, err in
onCompletion(json as JSON)
})
}
func makeHTTPGetRequest(path: String, onCompletion: ServiceResponse) {
let request = NSMutableURLRequest(URL: NSURL(string: path)!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
let json:JSON = JSON(data: data)
onCompletion(json, error)
})
task.resume()
}
func addData() {
APITest.sharedInstance.getRandomUser { json in
let historyData = json["response"]["history"]["date"]["pretty"]
dispatch_async(dispatch_get_main_queue(),{
println("Value:\(historyData)")
})
}
}
However, every time I run the code, it returns a null value. The API is in my code; refer to it as needed. Where did my code go wrong, and how can I fix it?
In the JSON I get from this API, the history dictionary is not inside the response dictionary but at the same root level.
So your code should be like this:
let historyData = json["history"]["date"]["pretty"]