What type to use for generic decodable class - swift

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
}

Related

Is Combine always required to make serial HTTP requests?

(note: I'm a beginner to programming) I just have a semantic question regarding Combine. I was under the impression that Combine was always required for serial HTTP requests but while experimenting I found that the following worked just fine:
// this function works even though it depends on the output of the first HTTP request
#available(iOS 15.0, *)
func displayPictures(completion: #escaping ([(AsyncImage<Image>, UUID)]) -> Void) throws {
do {
try getPictures { urls in
var tempImageArr = [(AsyncImage<Image>, UUID)]()
for url in urls {
guard let url = URL(string: url) else {
print("Invalid URL")
return
}
let image = AsyncImage(url: url)
let id = UUID()
tempImageArr.append((image, id))
}
completion(tempImageArr)
}
} catch {
throw NetworkError.failedToGetPictures
}
}
func getPictures(completion: #escaping ([String]) -> Void) throws {
guard let url = URL(string: "https://randomuser.me/api/?results=10&inc=picture") else {
print("Invalid URL")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
let decoder = JSONDecoder()
let decodedResponse = try! decoder.decode(RandomUserModel.self, from: data!)
completion(decodedResponse.pictures)
}.resume()
}
I'm hoping someone can clear up my confusion.
Thanks everyone.

Swift Rest API call example using Codable

I am following a tutorial on REST API calls with Swift and Codable. I cannot compile the following although I was careful when I typed all of it. Can anyone tell me what's wrong? Also, can anyone point me to a better tutorial? The error is:
Catch block is unreachable
and also
cannot find json in scope
import UIKit
import Foundation
struct Example: Codable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func getJson(completion: #escaping (Example)-> ()) {
let urlString = "https://jsonplaceholder.typicode.com/todos/1"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) {data, res, err in
if let data = data {
let decoder = JSONDecoder()
do {
let json: Example = try! decoder.decode(Example.self, from: data)
completion(json)
}catch let error {
print(error.localizedDescription)
}
}
}.resume()
}
}
getJson() { (json) in
print(json.id)
}
}
struct Example: Decodable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
struct APIRequest {
var resourceURL: URL
let urlString = "https://jsonplaceholder.typicode.com/todos/1"
init() {
resourceURL = URL(string: urlString)!
}
//create method to get decode the json
func requestAPIInfo(completion: #escaping(Result<Example, Error>) -> Void) {
let dataTask = URLSession.shared.dataTask(with: resourceURL) { (data, response, error) in
guard error == nil else {
print (error!.localizedDescription)
print ("stuck in data task")
return
}
let decoder = JSONDecoder()
do {
let jsonData = try decoder.decode(Example.self, from: data!)
completion(.success(jsonData))
}
catch {
print ("an error in catch")
print (error)
}
}
dataTask.resume()
}
}
class ViewController: UIViewController {
let apiRequest = APIRequest()
override func viewDidLoad() {
super.viewDidLoad()
apiRequest.requestAPIInfo { (apiResult) in
print (apiResult)
}
}
}
Instead of "do catch", you can use "guard let"
func getJson(completion: #escaping (Example)-> ()) {
let urlString = "https://jsonplaceholder.typicode.com/todos/1"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) {data, res, err in
guard let data = data else {return print("error with data")}
let decoder = JSONDecoder()
guard let json: Example = try? decoder.decode(Example.self, from: data) else {return print("error with json")}
completion(json)
}.resume()
}
}
This does NOT handle errors, so my solution is just to an answer to your question, not a general solution for every similar cases.

MVC Networking Swift

I have this Networking class that i declared in the Model .
class Networking {
func response (url : String ) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: urlPathCompletionHandler(data:response:error:)).resume()
}
func urlPathCompletionHandler (data : Data? , response: URLResponse? , error: Error? ) {
guard let data = data else {return }
do {
let jsondecoder = JSONDecoder()
}catch {
print("Error \(error)")
}
}
}
In the controller . I have an array of users i declared and i want the controller to call from the Model Networking class instead of doing the networking inside the controller. This is part of my controller:
var users = [Users]()
var networking : Networking()
#IBOutlet weak var tableview : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
}
func getFromModel() {
var vm = networking.response()
}
I want a way of calling the networking class and return an array of users that i can set to the users array above and use it to populate the table view . If i wanted to do that inside the controller it would easy but i am not sure how i can return an array of users from the Model Networking class .
You need to modify your Network class like this:
class Networking {
func response<T: Codable>(url: String, completion: ((T) -> Void)?) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
self.urlPathCompletionHandler(data: data, response: response, error: error, completion: completion)
}).resume()
}
func urlPathCompletionHandler<T: Codable>(data : Data? , response: URLResponse? , error: Error?, completion: ((T) -> Void)?) {
guard let data = data else { return }
do {
let jsondecoder = JSONDecoder()
// Pseudo Code to decode users
completion?(decodedObject)
} catch {
print("Error \(error)")
}
}
}
And call it like this:
func getFromModel() {
networking.response(url: <#T##String#>) { (users: [User]) in
self.users = users
}
}
OK, there are a few thoughts:
Your response method is performing an asynchronous network request, so you need to give it a completion handler parameter. So, I might suggest something like:
class Networking {
enum NetworkingError: Error {
case invalidURL
case failed(Data?, URLResponse?)
}
private let parsingQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".parsing")
// response method to handle network stuff
func responseData(_ string: String, completion: #escaping (Result<Data, Error>) -> Void) {
guard let url = URL(string: string) else {
completion(.failure(NetworkingError.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
completion(.failure(NetworkingError.failed(data, response)))
return
}
completion(.success(responseData))
}
}.resume()
}
// response method to handle the JSON parsing
func response<T: Decodable>(of type: T.Type, from string: String, completion: #escaping (Result<T, Error>) -> Void) {
responseData(string) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
self.parsingQueue.async {
do {
let responseObject = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(responseObject))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
}
}
}
}
This obviously assumes that you have some Codable types. For example, it’s common for an API to have some common structure in its responses:
struct ResponseObject<T: Decodable>: Decodable {
let code: Int
let message: String?
let data: T?
}
And maybe the User is like so:
struct User: Decodable {
let id: String
let name: String
}
Then getFromModel (perhaps better called getFromRepository or something like that) could parse it with:
networking.response(of: ResponseObject<[User]>.self, from: urlString) { result in
switch result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
// do something with users
}
}
For what it’s worth, if you didn’t want to write your own networking code, you could use Alamofire, and then getFromModel would do:
AF.request(urlString).responseDecodable(of: ResponseObject<[User]>.self) { response in
switch response.result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
}
}
Now, clearly the model types are likely to be different in your example, but you didn’t share what your JSON looked like, so I had to guess, but hopefully the above illustrates the general idea. Make a generic-based network API and give it a completion handler for its asynchronous responses.

Generic parameter 'T' could not be inferred: in Common Webservice method

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.

do not know how to get the result of completion

I am having trouble to use the result of a completion handler.
I am getting this error "Cannot convert value of type '()' to expected argument type"
struct SearchCollectionViewModel {
let name: String
let previewURL: String?
var image:UIImage?
let dataController = DataController()
}
extension SearchCollectionViewModel {
init(with result: Result) {
self.name = result.trackName
self.previewURL = result.previewURL
if let url = result.previewURL {
let imgData = preview(with: url, completion: { data -> Data? in
guard let data = data as? Data else { return nil }
return data
})
self.image = UIImage(data: imgData)
}
}
private func preview(with url: String, completion: #escaping (Data) -> Data?) {
dataController.download(with: url) { data, error in
if error == nil {
guard let imageData = data else { return }
DispatchQueue.main.async {
_ = completion(imageData)
}
}
}
}
}
A couple of observations:
You cannot “return” a value that is retrieved asynchronously via escaping closure.
The closure definition (Data) -> Data? says that the closure not only will be passed the Data retrieved for the image, but that the closure will, itself, return something back to preview. But it’s obviously not doing that (hence the need for _, as in _ = completion(...)). I’d suggest you change that to (Data?) -> Void (or use the Result<T, U> pattern).
I’d suggest renaming your Result type as there’s a well-known generic called Result<Success, Failure> for returning .success(Success) or .failure(Failure). This is a pattern that we’ve used for a while, but is formally introduced in Swift 5, too. See SE-0235.
Your codebase can have its own Result type, but it’s an invitation for confusion later down the road if and when you start adopting this Result<T, U> convention.
You really shouldn’t be initiating asynchronous process from init, but instead invoke a method to do that.
Personally, I’d move the conversion to UIImage into the DataController, e.g.
extension DataController {
func downloadImage(with url: URL, completion: #escaping (UIImage?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
let image = data.flatMap { UIImage(data: $0) }
completion(image, error)
}
task.resume()
}
}
So, I might suggest you end up with something like:
class SearchCollectionViewModel {
let name: String
let previewURL: String?
let dataController = DataController()
var image: UIImage?
init(with result: Result) {
self.name = result.trackName
self.previewURL = result.previewURL
}
}
extension SearchCollectionViewModel {
func preview(with url: String, completion: #escaping (UIImage?) -> Void) {
guard let urlString = previewURL, let url = URL(string: urlString) else {
completion(nil)
return
}
dataController.downloadImage(with: url) { [weak self] image, error in
DispatchQueue.main.async {
self?.image = image
completion(image)
}
}
}
}