Swift generic Serializable protocol - swift

I noticed that I have a lot of repeated code for getting/sending JSON to my app's API, but the only thing that is different is what entity is being (de)serialized. So I came up with the following design (for brevity only the GET method):
HTTPClient.swift
func getEntityJSON<T: JSONSerializable>(
type: T.Type,
url: String,
completionHandler: #escaping (_ result: T?,
_ headers: [String: String]?,
_ statusCode: Int?,
_ error: Error?) -> Void) {
HttpClient.sharedInstance().getJSON(url, completionHandler: { (jsonData, headers, statusCode, error) in
if let error = error {
completionHandler(nil, headers, statusCode, error)
} else {
if let entityData = jsonData {
completionHandler(T.fromJSON(data: entityData), headers, statusCode, nil)
}
}
})
}
to intend to use it like this:
HTTPClient.sharedInstance().getEntity(type: Translation, url: url) { (translation, _, _, _) in
// do stuff with Translation instance
}
and here is the JSONSerializable protocol:
import Foundation
import SwiftyJSON
protocol Serializable: Codable {
static func deserialize<T: Codable>(data: Data) -> T?
func serialize() -> Data?
}
extension Serializable {
static func deserialize<T: Decodable>(data: Data) -> T? {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
return try? decoder.decode(T.self, from: data)
}
func serialize() -> Data? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .formatted(DateFormatter.iso8601Full)
return try? encoder.encode(self)
}
}
protocol JSONSerializable: Serializable {
func toJSON() -> JSON?
static func fromJSON<T>(data: JSON) -> T?
}
extension JSONSerializable {
func toJSON() -> JSON? {
if let data = self.serialize() {
return try? JSON(data: data)
} else {
return nil
}
}
}
This allows me to define a struct like this:
Translation.swift
struct Translation: Hashable, Identifiable, JSONSerializable {
var id: UUID
...
static func fromJSON(data: JSON) -> Translation? {
and I will get serialize, deserialize, toJSON functions. But the compiler complains that Translation does not conform to JSONSerializable, due to lack of:
static func fromJSON<T>(data: JSON) -> T?. I thought I would be able to implement that function with a concrete type, Translation in this case.
I would like to be both able to do Translations.fromJSON(data: data) as well as T.fromJSON(data: data). How can I achieve this?

Generics (<T>) means that your method can work with any type that the caller (not the callee, not the implementer, not anyone else) specifies. In the case of fromJSON, this is clearly not the case. Any concrete implementation of fromJSON only works with T being the enclosing type, so fromJSON is not suitable to be generic.
When a method "only works with T being the enclosing type", it is the use case of Self types:
protocol JSONSerializable: Serializable {
func toJSON() -> JSON?
static func fromJSON(data: JSON) -> Self?
}
This way, your Translation implementation would conform to the protocol.
Similarly, deserialise should also be declared as:
static func deserialize(data: Data) -> Self?
If you actually want a JSONSerialisable that can turn JSON into any type that the caller wants, then it's less of a JSONSerialisable, and more of a JSONSerialiser, and it doesn't make much sense to inherit from Codable in that case.

OK I ended up implementing a JSONSerializer, because I couldn't get it to work for collections of items to be (de)serialized. This is how it currently looks:
import Foundation
import SwiftyJSON
protocol JSONSerializable {
static func fromJSON(data: JSON) -> Self?
}
class JSONSerializer {
static func deserialize<T: Decodable>(_ type: T.Type, data: Data) -> T? {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
return try? decoder.decode(type, from: data)
}
static func serialize<T: Encodable>(_ object: T) -> Data? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .formatted(DateFormatter.iso8601Full)
return try? encoder.encode(object)
}
static func fromJSON<T: JSONSerializable>(type: T.Type, data: JSON) -> T? {
T.fromJSON(data: data)
}
static func toJSON<T: Encodable>(_ object: T) -> JSON? {
if let data = Self.serialize(object) {
return try? JSON(data: data)
} else {
return nil
}
}
}
I will look into the fromJSON function later to see if I can completely get rid of JSONSerializable and implemented it in the JSONSerializer like this:
static func fromJSON<T: Decodable>(type: T.Type, data: JSON) -> T? {
if let jsonData = try? data.rawData() {
return Self.deserialize(type, data: jsonData)
}
return nil
}

Related

Method cannot be marked #objc because the type of the parameter 3 cannot be represented in Objective-C - Error type not recognized in #objc func?

I’m trying to call this function from an objective C class but am running into an issue. I get the error "Method cannot be marked #objc because the type of the parameter 3 cannot be represented in Objective-C"
I’m assuming the issue is with the “Error” parameter but don’t understand what the issue is exactly. Everything I’ve read has been dealing with Swift 1 or 2 but this is in Swift 5.
class NetworkManager {
struct DataModel: Decodable {
let id: String
let carID: String
let ownerId: String
let locationId: String?
let status: String
}
static let shared = NetworkManager()
#objc static func fetchCarWarranty(for carID: String, ownerID: String, completed: #escaping (Result<[DataModel], Error>) -> Void) {
let urlString = APIURL
let session = URLSession.shared
let url = URL(string: urlString)!
var request = URLRequest(url: url)
let task = session.dataTask(with: request) { data, response, error in
if let _ = error {
completed(.failure(error!))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completed(.failure(error!))
return
}
guard let data = data else {
return
}
do {
let decoder = JSONDecoder()
let dataModel = try decoder.decode([DataModel].self, from: data)
completed(Result.success(dataModel))
} catch {
completed(.failure(error))
}
}
task.resume()
}
}
I've thought of calling fetchCarWarranty from a second Swift function but not sure if this is a good practice to follow or not.
Also, how can I get the value from this function if I'm calling it from an objective-c file/ class?
You can't use Result in Objective-C, you need a closure like the one for dataTask(with:completion:), with optional parameters like error to indicate it's not in "error case":
//Call that one when using Objective-C
#objc static func fetchCarWarranty(for carID: String, ownerID: String, completed: #escaping (([DataModel]?, Error?) -> Void)) {
fetchCarWarranty(for: carID, ownerID: ownerID) { result in
switch result {
case .success(let models):
completed(models, nil)
case .failure(let error):
completed(nil, error)
}
}
}
//Call that one when using Swift
static func fetchCarWarranty(for carID: String, ownerID: String, completed: #escaping (Result<[DataModel], Error>) -> Void) {
// Your method "Swifty" code
}
I did this for an app with legacy Objective-C code. You call in the end the Swifty method with Result internally. so the code does the same.
You could also for the Objective-C compatible one have to closures:
#objc static func fetchCarWarranty(for carID: String, ownerID: String, success: #escaping (([DataModel]) -> Void), failure: #escaping ((Error) -> Void)) {
fetchCarWarranty(for: carID, ownerID: ownerID { result in
switch result {
case .success(let models):
success(models)
case .failure(let error):
failure(error)
}
}
}
Now, the issue is that DataModel is a struct. If you replace that with:
// With temp [String] parameter instead of [DataModel]
#objc static func fetchCarWarranty(for carID: String, ownerID: String, completed: #escaping (([String]?, Error?) -> Void)) {
fetchCarWarranty(for: carID, ownerID: ownerID) { result in
switch result {
case .success(let models):
completed(["models"], nil) //Fake output
case .failure(let error):
completed(nil, error)
}
}
}
It works, because String is visible by Objective-C. But you can't see a Swift struct from Objective-C. See the documentation, or one the related questions:
Is there a way to use Swift structs in Objective-C without making them Classes?
How to pass a Swift struct as a parameter to an Objective-C method
How to use Swift struct in Objective-C

Trying to get data with Alamofire and swift 5

I want to load some data with Alamofire
Thats is my class
struct Welcome: Codable {
let count: Int
let next: String
let previous: String
let results: [Übungen]
}
// MARK: - Result
struct Übungen: Codable {
let id, category: Int
let resultDescription, name, nameOriginal: String
let muscles, musclesSecondary, equipment: Int
let creationDate: String
let language: Int
let uuid: String
let variations: Int
next would be my Protocol
protocol ÜbungenGateway {
func fetch(completion: #escaping (Result<[Übungen], Error>) -> Void)
}
and as the last part comes the Alamofireclass
class ÜbungenAlamofireGateway : ÜbungenGateway {
let url = "https://wger.de/api/v2/"
func fetch(completion: #escaping (Result<[Übungen], Error>) -> Void) {
AF.request(url)
.validate()
.responseJSON {
switch $0.result{
case .success:
do {
let decoder = JSONDecoder()
let excercise = try! decoder.decode(Übungen.self, from: $0.data!)
completion(.success(excercise.name))
}catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
my Problem is that i get the following error:
"Cannot convert value of type 'String' to expected argument type '[Übungen]'"
at the line
completion(.success(excercise.name))
and i dont know why.
I hope someone could help me out.
So the first issue is with type in completion block in fetch method.
func fetch(completion: #escaping (Result<[Übungen], Error>) -> Void)
Expected returned type in completion is a list of the Übungen object, but you are trying to pass name (which is a string) from Übungen object completion(.success(excercise.name)). That's why you are getting this error.
The second think is that you are calling "https://wger.de/api/v2/" endpoint and from documentation the response will be a list of links and some properties from your object does not exist in the response.

How can I cast a NSManagedObject to NSItemProviderWriting/NSItemProviderReading?

I wanna implement the function that drag a tableview cell on a "Delete Icon" to delete it.
Now my question is how can I cast my Type to NSItemProviderWriting/NSItemProviderReading to use drag and drop.
I'm following this tutorial: https://exploringswift.com/blog/creating-a-nsitemprovider-for-custom-model-class-drag-drop-api. While I failed and I still could not understand how it works.
It says that Type 'Task' does not conform to protocol 'Decodable'.('Task' is my custom model) and I also don't know what 'kUTTypeData' is in this tutorial...
Can anyone help to how to implement these protocol?
import Foundation
import CoreData
#objc(Task)
public class Task: NSManagedObject, NSItemProviderWriting, NSItemProviderReading, Codable {
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
<#code#>
}
required public init(from decoder:Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
}
public static var writableTypeIdentifiersForItemProvider: [String] {
return []
}
public func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: #escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
do {
let encoder = JSONEncoder()
let data = try encoder.encode(self)
progress.completedUnitCount = 100
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
return progress
}
public static var readableTypeIdentifiersForItemProvider: [String] {
return []
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
let decoder = JSONDecoder()
do {
let myJSON = try decoder.decode(Task.self, from: data)
return myJSON as! Self
} catch {
fatalError("Err")
}
}
}
Instead of making the NSManagedObject conform to the protocols, have you considered using .objectID.uriRepresentation() in the drag/drop handlers?
Here you can find an explanation for implementing Codable with Core Data entities:
https://stackoverflow.com/a/46917019

Generic Decoder for Swift using a protocol

I tried to use a generic Json Decoder for all of my models using a protrocol.
//Here the definition of the protocol:
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Decodable) -> T?, completion: #escaping (Result<T, APIError>) -> Void) {.. other Code}
//Here the implementation:
func getData(from endPoint: Endpoint, completion: #escaping (Result<ApiResponseArray<Codable>, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { json -> Decodable in
guard let dataResult = json as? modelData else { return nil }
return dataResult
}, completion: completion)
}
ApiResponseArray gives me the error: Protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable' because only concrete types can conform to protocols. But how can I implement a generic decoder and passing them different models. I think I have to modify my protocol definition but how? I would like to pass the model and then receive the decoded data for the model (in my example modelData). Its obvious that the program runs when I write:
func getData(from endPoint: Endpoint, completion: #escaping (Result, APIError>) I mean when I use the concrete Model, but I want to pass the model, so that I can use the class for different models.
Thanks,
Arnold
A protocol cannot conform to itself, Codable must be a concrete type or can only be used as a generic constraint.
In your context you have to do the latter, something like this
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Data) throws -> T, completion: #escaping (Result<T, APIError>) -> Void) { }
func getData<T: Decodable>(_ : T.Type = T.self, from endPoint: Endpoint, completion: #escaping (Result<T, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { data -> T in
return try JSONDecoder().decode(T.self, from: data)
}, completion: completion)
}
A network request usually returns Data which is more reasonable as parameter type of the decode closure
I can suggest to you how to use Decodable with your API call structure by using Alamofire.
I have created RequestManager class which inherits from SessionManager and added request call inside which common to all.
class RequestManager: SessionManager {
// Create shared instance
static let shared = RequestManager()
// Create http headers
lazy var httpHeaders : HTTPHeaders = {
var httpHeader = HTTPHeaders()
httpHeader["Content-Type"] = "application/json"
httpHeader["Accept"] = "application/json"
return httpHeader
}()
//------------------------------------------------------------------------------
// MARK:-
// MARK:- Request Methods
//------------------------------------------------------------------------------
func responseRequest(_ url: String, method: Alamofire.HTTPMethod, parameter: Parameters? = nil, encoding: ParameterEncoding, header: HTTPHeaders? = nil, completionHandler: #escaping (DefaultDataResponse) -> Void) -> Void {
self.request(url, method: method, parameters: parameter, encoding: encoding, headers: header).response { response in
completionHandler(response)
}
}
}
Then after one more class created NetworkManager class which hold required get/post method call and decode json by JSONDecoder as follow:
class NetworkManager {
static let shared = NetworkManager()
var progressVC : ProgressVC?
//----------------------------------------------------------------
// MARK:-
// MARK:- Get Request Method
//----------------------------------------------------------------
func getResponse<T: Decodable>(_ url: String, parameter: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, header: HTTPHeaders? = nil, showHUD: HUDFlag = .show, message: String? = "Please wait...", decodingType: T.Type, completion: #escaping (Decodable?, APIError?) -> Void) {
DispatchQueue.main.async {
self.showHideHud(showHUD: showHUD, message: "")
}
RequestManager.shared.responseRequest(url, method: .get, parameter: parameter, encoding: encoding, header: header) { response in
DispatchQueue.main.async {
self.showHideHud(showHUD: .hide, message: "")
}
guard let httpResponse = response.response else {
completion(nil, .requestFailed("Request Failed"))
return
}
if httpResponse.statusCode == 200 {
if let data = response.data {
do {
let genericModel = try JSONDecoder().decode(decodingType, from: data)
completion(genericModel, nil)
} catch {
do {
let error = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
if let message = error!["message"] as? String {
completion(nil, .errorMessage(message)!)
} else if let message = error!["message"] as? Int {
completion(nil, .errorMessage(String(describing: "Bad Request = \(message)")))
}
} catch {
completion(nil, .jsonConversionFailure("JSON Conversion Failure"))
}
}
} else {
completion(nil, .invalidData("Invalid Data"))
}
} else {
completion(nil, .responseUnsuccessful("Response Unsuccessful"))
}
}
}
}
ProgressVC is my custom class to show progress view when api call.
After that, I have created DataManager class which will help me to create request url.
class DataManager: NSObject {
//------------------------------------------------------------------------------
// MARK:- Variables
//------------------------------------------------------------------------------
static let shared = DataManager()
let baseUrl = WebServiceURL.local
//------------------------------------------------------------------------------
// MARK:- Custom Methods
//------------------------------------------------------------------------------
// Get API url with endpoints
func getURL(_ endpoint: WSEndPoints) -> String {
return baseUrl + endpoint.rawValue
}
}
I have created following enum to send data or error in my completion block.
enum Result<T, U> where U: Error {
case success(T)
case failure(U)
}
Here is list of error which stored custom message related to status fired during api call.
enum APIError: Error {
case errorMessage(String)
case requestFailed(String)
case jsonConversionFailure(String)
case invalidData(String)
case responseUnsuccessful(String)
case jsonParsingFailure(String)
var localizedDescription: String {
switch self {
case.errorMessage(let msg):
return msg
case .requestFailed(let msg):
return msg
case .jsonConversionFailure(let msg):
return msg
case .invalidData(let msg):
return msg
case .responseUnsuccessful(let msg):
return msg
case .jsonParsingFailure(let msg):
return msg
}
}
}
Then after, I will extend this DataManager class to call web service based on module. So I will create Swift file and extend DataManager class and call relative API.
See following, In API call I will return relative model into Result like Result<StoreListModel, APIError>
extension DataManager {
// MARK:- Store List
func getStoreList(completion: #escaping (Result<StoreListModel, APIError>) -> Void) {
NetworkManager.shared.getResponse(getURL(.storeList), parameter: nil, encoding: JSONEncoding.default, header: getHeaders("bd_suvlascentralpos"), showHUD: .show, message: "Please wait...", decodingType: StoreListModel.self) { (decodableData, apiError) in
if apiError != nil {
completion(.failure(apiError!))
} else {
guard let userData = decodableData as? StoreListModel else {
completion(.failure(apiError!))
return
}
completion(.success(userData))
}
}
}
}
From completion block of request I will get decodable data which here safely type cast.
Use:
DataManager.shared.getStoreList { (result) in
switch result {
case .success(let storeListModel):
if let storeList = storeListModel, storeList.count > 0 {
self.arrStoreList = storeList
self.tblStoreList.isHidden = false
self.labelEmptyData.isHidden = true
self.tblStoreList.reloadData()
} else {
self.tblStoreList.isHidden = true
self.labelEmptyData.isHidden = false
}
break
case .failure(let error):
print(error.localizedDescription)
break
}
}
Note:- Some variables, models classes are my custom. You can replace it with your.

I have an error generating my model from QuickType in xcode with swift

I am working with alamofire 4.0, I generate my models from QuickType a very useful tool, but I am getting this error. In the extension of Alamofire, I do not understand what can happen since all this class returns the QuickType console.
This is the model I got from quicktype with Alamofire extension
import Foundation
import Alamofire
class LoginResponse: Codable {
let user: JSONNull?
let status: Int
let success: Bool
let message: String
init(user: JSONNull?, status: Int, success: Bool, message: String) {
self.user = user
self.status = status
self.success = success
self.message = message
}
}
// MARK: Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
fileprivate func newJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
decoder.dateDecodingStrategy = .iso8601
}
return decoder
}
fileprivate func newJSONEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
encoder.dateEncodingStrategy = .iso8601
}
return encoder
}
// MARK: - Alamofire response handlers
extension DataRequest {
fileprivate func decodableResponseSerializer<T: Decodable>() -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
guard error == nil else { return .failure(error!) }
guard let data = data else {
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
}
return Result { try newJSONDecoder().decode(T.self, from: data) }
}
}
#discardableResult
fileprivate func responseDecodable<T: Decodable>(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<T>) -> Void) -> Self {
return response(queue: queue, responseSerializer: decodableResponseSerializer(), completionHandler: completionHandler)
}
#discardableResult
func responseLoginResponse(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<LoginResponse>) -> Void) -> Self {
return responseDecodable(queue: queue, completionHandler: completionHandler)
}
}
the error is in this line of code fileprivate func decodableResponseSerializer<T: Decodable>() -> DataResponseSerializer<T>:
fileprivate func decodableResponseSerializer<T: Decodable>() -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
guard error == nil else { return .failure(error!) }
guard let data = data else {
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
}
return Result { try newJSONDecoder().decode(T.self, from: data) }
}
}
and this says
Cannot specialize non-generic type 'DataResponseSerializer'