Swift Generic func calling a Generic func - swift

I have an interesting compile error when using one generic to call another. This post is a bit long but hopefully, it's not longer than needed to describe the problem.
I have defined a generic func that works great. I use it a lot and the pattern of use is often the same. I was trying to implement a new generic func that nests the existing generic func, but I'm getting a compile-time error.
For some context, here is how my API works with the generic now. My REST API makes a call the films() func, which hits the StarWarsAPI (swapi.co) and returns a list of all the Star Wars films they have in their database, as follows:
StarWarsAPI.shared.films(){ (films, error) in
for film in films {
print(film.title)
}
}
The films() function calls a generic (restCall()) which works great. Here is the definition of films():
public func films(completion: #escaping (_ films:[Film]?, _ error:StarWarsError?) -> Void) {
guard StarWarsAPI.isOperational else {
return completion(nil,StarWarsError.starWarsAPINotOperational)
}
restCall(fetchUrl: filmsUrl!, modelType: FilmResult()) { (filmResults, error ) in
completion(filmResults?.results, error)
}
}
Where the restCall (the generic) is defined as follows: (Note I'm using the Swift 4 Codable API)
public func restCall<T: Codable>(fetchUrl: URL, modelType: T, completion: #escaping (_ modelObject: T?, _ error:StarWarsError?) -> Void){
var fetchRequest = URLRequest(url: fetchUrl, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
fetchRequest.httpMethod = "GET"
fetchRequest.allHTTPHeaderFields = [
"content-type": "application/json",
"cache-control": "no-cache",
]
let session = URLSession.shared
let fetchDataTask = session.dataTask(with: fetchRequest) { (data, response, error) in
DispatchQueue.main.async { // return to main thread
var modelObject:T?
do {
let jsonDecoder = JSONDecoder()
modelObject = try jsonDecoder.decode(T.self, from: data)// FIXME: Something about decoding the People object is going wrong.
return completion(modelObject, nil)
}catch let error as NSError {
completion(nil, StarWarsError.decodingModelError(error: error))
}
}
}
fetchDataTask.resume()
}
So the above works great. I use it for rest functions films(), people(), ships(), etc. I use the same pattern for each rest call. I want to create a generic that I can use instead of explicit films(), people(), etc.
I've been trying to get the following to work with little success:
public func fetchAll<T: Result>(result:T, completionAll: #escaping (_ result:T?, _ error:StarWarsError?) -> Void) {
restCall(fetchUrl: result.urlPath!, modelType: T) { (finalResults, error ) in
completionAll(finalResults!, error)
}
}
Where Result type is the base type and is defined as follows:
public class Result {
var urlPath:URL?
}
public class FilmResult: Result, Codable {
var count:Int?
var next:String?
var previous:String?
var results:[Film]?
}
The error I'm getting is shown in the screenshot below - hopefully, it's clear.
Any help you can provide would be much appreciated!

Your call has to be
this was T previously ───┐
restCall(fetchUrl: result.urlPath!, modelType: result) { (finalResults, error ) in
Note the result instead of T.
Relatively minimal code to reproduce:
public func restCall<T>(fetchUrl: URL, modelType: T, completion: #escaping (_ modelObject: T?, _ error:String?) -> Void) { }
public func fetchAll<T>(result:T, completionAll: #escaping (_ result:T?, _ error:String?) -> Void) {
┌── should be result
restCall(fetchUrl: URL(string: "asdasd")!, modelType: T) { (finalResults, error ) in
completionAll(finalResults, error)
}
}

Related

Shorthand conversion from Result to Future in Swift?

I'd like to provide Combine counterparts to completion closures which is becoming very cumbersome. Is there a shorter way or extension that can convert the following:
extension MyType {
func send(with request: URLRequest, completion: #escaping (Result<MyResponse, MyError>) -> Void) {
// Logic
}
func send(with request: URLRequest) -> Future<MyResponse, MyError> {
Future<MyResponse, MyError> { promise in
send(with: request) { result in
switch result {
case let .success(response):
promise(.success(response))
case let .failure(error):
promise(.failure(error))
}
}
}
}
}
The Future method is just a wrapper to the completion closure method. I was hoping to do at least something like this:
Future<MyResponse, MyError> { send(with:request, completion: $0) }
Is there a more elegant way to do this since this will be applied in a lot of places in my library.
Note that the completion parameter of the first send overload has the type:
Result<MyResponse, MyError>) -> Void
Which is exactly the same type as promise, which is
Future<MyResponse, MyError>.Promise
Promise is just a type alias for (Result<Output, Failure>) -> Void.
So you can just do:
Future<MyResponse, MyError> { promise in
send(with: request, completion: promise)
}

Generic type 'Result' specialized with too few type parameters (got 1, but expected 2)

I just wanted to include Result in my project and am running across a few issues. It seems to me as if Alamofire (which is already a dependency) defines its own Result type throwing problems when trying to write functions that return results.
For example Xcode (10.2 beta 4) tells me that I can't write Result-> Response = (_ result: Result) -> Void because Generic type 'Result' specialized with too few type parameters (got 1, but expected 2).
Both are linked as frameworks installed via Cocoapods in a "Swift 5.0 beta" project.
I'm guessing issues like this shouldn't actually be occurring, but I'm doing something wrong here. Any pointers would be great, thank you!
import Foundation
import Alamofire
typealias Response<T> = (_ result: Result<T>) -> Void //error here
class APIClient {
private static let baseUrl: URL = URL(string: "https://api.flickr.com/services/rest/")!
private static let key: String = "8e15e775f3c4e465131008d1a8bcd616"
private static let parameters: Parameters = [
"api_key": key,
"format": "json",
"nojsoncallback": 1
]
static let shared: APIClient = APIClient()
let imageCache = NSCache<NSString, UIImage>()
#discardableResult
private static func request<T: Decodable>(path: String? = nil,
method: HTTPMethod,
parameters: Parameters?,
decoder: JSONDecoder = JSONDecoder(),
completion: #escaping (Result<T>) -> Void) -> DataRequest {
let parameters = parameters?.merging(APIClient.parameters, uniquingKeysWith: { (a, _) in a })
return AF.request(try! encode(path: path, method: method, parameters: parameters))
.responseDecodable (decoder: decoder) { (response: DataResponse<T>) in completion(response.result) }
}
You can qualify the reference to Result in order to choose the one you want. The version with one parameter belongs to Alamofire. The one with two parameters belongs to Swift.
typealias Response<T> = (_ result: Alamofire.Result<T>) -> Void
... or ...
static func upload(
data: Data,
completion: #escaping (Swift.Result<Int, Error>) -> Void
)
In Alamofire 5.1.0 changing :
typealias Response<T> = (_ result: Result<T>) -> Void
to
typealias Response<T> = (_ result: AFResult<T>) -> Void
worked.

Cannot convert value of type 'MyEnum<T.Type>' to expected argument type 'MyEnum<_>'

I have a network layer working with generics and I'm using protocols so I can test it later. I have followed this tutorial https://medium.com/thecocoapps/network-layer-in-swift-4-0-972bf2ea5033
This is my Mock for testing:
import Foundation
#testable import TraktTest
class MockUrlSessionProvider: ProviderProtocol {
enum Mode {
case success
case empty
case fail
}
private var mode: Mode
init(mode: Mode) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
switch mode {
case .success: completion(NetworkResponse.success(T))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}
I'm getting the error: Cannot convert value of type 'NetworkResponse<T.Type>' to expected argument type 'NetworkResponse<_>' in this line: completion(NetworkResponse.success(T))
If I send this to my completion success it compile:
try? JSONDecoder().decode(T.self, from: data!)
(dummy data that I created using encode and my model), but crash when get to my model because is nil despite I had encoded using JSONEncoder() with a correct model.
I think it works, because is the same logic that I use in my class that implements ProviderProtocol in my app:
final class URLSessionProvider: ProviderProtocol {
private var session: URLSessionProtocol
init(session: URLSessionProtocol = URLSession.shared) {
self.session = session
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
let request = URLRequest(service: service)
session.dataTask(request: request) { [weak self] data, response, error in
let httpResponse = response as? HTTPURLResponse
self?.handleDataResponse(data: data, response: httpResponse, error: error, completion: completion)
}.resume()
}
private func handleDataResponse<T: Decodable>(data: Data?, response: HTTPURLResponse?, error: Error?, completion: (NetworkResponse<T>) -> Void) {
guard error == nil else { return completion(.failure(.unknown(error?.localizedDescription ?? "Error"))) }
guard let response = response else { return completion(.failure(.unknown("no_response".localized()))) }
switch response.statusCode {
case 200...299:
guard let data = data, let model = try? JSONDecoder().decode(T.self, from: data) else { return completion(.failure(.noData)) }
completion(.success(model))
default: completion(.failure(.unknown("no_response".localized())))
}
}
}
URLSessionProtocol is just a protocol which has a method dataTask same as the one in URLSession.shared (receive a URLRequest and returns Data, Response and Error in a completion).
My Network responses are a couple of enums:
enum NetworkResponse<T> {
case success(T)
case failure(NetworkError)
}
enum NetworkError {
case unknown(String)
case noData
}
My provider protocol just have a function to make the request using generics:
protocol ProviderProtocol {
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping(NetworkResponse<T>) -> Void) where T: Decodable
}
I don't think I need to use ServiceProtocol in my test because is to setup the request with endpoint, headers, body, id, etc. But this is the protocol I created:
typealias Headers = [String: String]
typealias Parameters = [String: Any]
protocol ServiceProtocol {
func baseURL() -> URL
var path: String? { get }
var id: String? { get }
var method: HTTPMethod { get }
var task: Task { get }
var headers: Headers? { get }
var parametersEncoding: ParametersEncoding { get }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum Task {
case requestPlain
case requestParameters(Parameters)
}
enum ParametersEncoding {
case url
case json
}
In my app, I have a class that implement ProviderProtocol and use a URLSession.shared to make the dataTask when some viewModel call the request with the appropiate model.
I'm use to make test with protocols and a specific model, but with generics is showing me that error. How can I achieve to have a mock provider using generics so I can test any viewModel who make a call to network using different kinds of models (stubs).
The error occurs because NetworkResponse expects an instance of T, while the mock tries to provide the actual T.
So, you need to somehow provide an instance, however this cannot be generated by the mock as it doesn't have enough information about how to construct an instance.
I recommend injecting the success value from the outside, when creating the mock. You can do this either by making the mock class generic, or by making the Mode enum generic. Below is a sample implementation for the latter:
class MockUrlSessionProvider: ProviderProtocol {
// making the enum generic, to support injecting the success value
enum Mode<T> {
case success(T)
case empty
case fail
}
// need to have this as `Any` to cover all possible T generic arguments
private var mode: Any
// however the initializer can be very specific
init<T>(mode: Mode<T>) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
// if the mock was not properly configured, do nothing
guard let mode = mode as? Mode<T> else { return }
// alternatively you force cast and have the unit test crash, this should help catching early configuration issues
// let mode = mode as! Mode<T>
switch mode {
case let .success(value): completion(NetworkResponse.success(value))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}

Custom Serialization finds incorrect "response" in Alamofire 4.0

I'm attempting to define a custom model serialization for Alamofire 4.0. So far I'm following the model presented used by responseJson and friends. Specifically, what I have so far is:
extension Alamofire.Request {
public static func serializeResponseModel<T:ModelObject>(response:HTTPURLResponse?, data:Data?, error:Error?) -> Alamofire.Result<T> {
switch serializeResponseJSON(options: [], response: response, data: data, error: error) {
case .success(let jsonObject):
do {
return .success(try T(json:jsonObject as! JSONObject))
}
catch {
return .failure(error)
}
case .failure(let error):
return .failure(error)
}
}
}
extension Alamofire.DataRequest {
public static func serializeResponseModel<T:ModelObject>() -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
return Request.serializeResponseConcierge(response: response, data: data, error: error)
}
}
#discardableResult
public func responseModel<T:ModelObject>(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<T>) -> Void) -> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.serializeResponseModel(),
completionHandler: completionHandler
)
}
}
Unfortunately, the framework is somewhat poorly implemented and the line return response( is finding the response property (defined in Request) and not the appropriate response method (defined in DataRequest), which leads to the compile error:
Cannot call value of non-function type 'HTTPURLResponse?'
What am I missing here that allows this to work in the responseJson case, but not in my case?
Apparently the problem arose from over-generalization, and the compiler not being able to generate an appropriate type for DataRequest.serializeResponseModel() When I changed responseModel to the following and specified the appropriate type, things work as expected:
#discardableResult
public func responseModel<T:ModelObject>(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<T>) -> Void) -> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.modelResponseSerializer() as DataResponseSerializer<T>,
completionHandler: completionHandler
)
}

How to pass a class type as a function parameter

I have a generic function that calls a web service and serialize the JSON response back to an object.
class func invokeService<T>(service: String, withParams params: Dictionary<String, String>, returningClass: AnyClass, completionHandler handler: ((T) -> ())) {
/* Construct the URL, call the service and parse the response */
}
What I'm trying to accomplish is is the equivalent of this Java code
public <T> T invokeService(final String serviceURLSuffix, final Map<String, String> params,
final Class<T> classTypeToReturn) {
}
Is my method signature for what I'm trying to accomplish correct?
More specifically, is specifying AnyClass as a parameter type the
right thing to do?
When calling the method, I'm passing MyObject.self as the returningClass value, but I get a compilation error "Cannot convert the expression's type '()' to type 'String'"
CastDAO.invokeService("test", withParams: ["test" : "test"], returningClass: CityInfo.self) { cityInfo in /*...*/
}
Edit:
I tried using object_getClass, as mentioned by holex, but now I get:
error: "Type 'CityInfo.Type' does not conform to protocol 'AnyObject'"
What need to be done to conform to the protocol?
class CityInfo : NSObject {
var cityName: String?
var regionCode: String?
var regionName: String?
}
You are approaching it in the wrong way: in Swift, unlike Objective-C, classes have specific types and even have an inheritance hierarchy (that is, if class B inherits from A, then B.Type also inherits from A.Type):
class A {}
class B: A {}
class C {}
// B inherits from A
let object: A = B()
// B.Type also inherits from A.Type
let type: A.Type = B.self
// Error: 'C' is not a subtype of 'A'
let type2: A.Type = C.self
That's why you shouldn't use AnyClass, unless you really want to allow any class. In this case the right type would be T.Type, because it expresses the link between the returningClass parameter and the parameter of the closure.
In fact, using it instead of AnyClass allows the compiler to correctly infer the types in the method call:
class func invokeService<T>(service: String, withParams params: Dictionary<String, String>, returningClass: T.Type, completionHandler handler: ((T) -> ())) {
// The compiler correctly infers that T is the class of the instances of returningClass
handler(returningClass())
}
Now there's the problem of constructing an instance of T to pass to handler: if you try and run the code right now the compiler will complain that T is not constructible with (). And rightfully so: T has to be explicitly constrained to require that it implements a specific initializer.
This can be done with a protocol like the following one:
protocol Initable {
init()
}
class CityInfo : NSObject, Initable {
var cityName: String?
var regionCode: String?
var regionName: String?
// Nothing to change here, CityInfo already implements init()
}
Then you only have to change the generic constraints of invokeService from <T> to <T: Initable>.
Tip
If you get strange errors like "Cannot convert the expression's type '()' to type 'String'", it is often useful to move every argument of the method call to its own variable. It helps narrowing down the code that is causing the error and uncovering type inference issues:
let service = "test"
let params = ["test" : "test"]
let returningClass = CityInfo.self
CastDAO.invokeService(service, withParams: params, returningClass: returningClass) { cityInfo in /*...*/
}
Now there are two possibilities: the error moves to one of the variables (which means that the wrong part is there) or you get a cryptic message like "Cannot convert the expression's type () to type ($T6) -> ($T6) -> $T5".
The cause of the latter error is that the compiler is not able to infer the types of what you wrote. In this case the problem is that T is only used in the parameter of the closure and the closure you passed doesn't indicate any particular type so the compiler doesn't know what type to infer. By changing the type of returningClass to include T you give the compiler a way to determine the generic parameter.
you can get the class of AnyObject via this way:
Swift 3.x
let myClass: AnyClass = type(of: self)
Swift 2.x
let myClass: AnyClass = object_getClass(self)
and you can pass it as paramater later, if you'd like.
I have a similar use case in swift5:
class PlistUtils {
static let shared = PlistUtils()
// write data
func saveItem<T: Encodable>(url: URL, value: T) -> Bool{
let encoder = PropertyListEncoder()
do {
let data = try encoder.encode(value)
try data.write(to: url)
return true
}catch {
print("encode error: \(error)")
return false
}
}
// read data
func loadItem<T: Decodable>(url: URL, type: T.Type) -> Any?{
if let data = try? Data(contentsOf: url) {
let decoder = PropertyListDecoder()
do {
let result = try decoder.decode(type, from: data)
return result
}catch{
print("items decode failed ")
return nil
}
}
return nil
}
}
Simply copy paste each code here into swift file:
# save as: APICaller.swift
import Foundation
struct APICaller
{
public static func get<T: Decodable>(url: String, receiveModel: T.Type, completion:#escaping (Decodable) -> ())
{
send(url: url, json: nil, receiveModel: receiveModel, completion: completion, httpMethod: "GET")
}
public static func post<T: Decodable>(url: String, json: [String: Any]?, receiveModel: T.Type, completion:#escaping (Decodable) -> ())
{
send(url: url, json: nil, receiveModel: receiveModel, completion: completion, httpMethod: "POST")
}
public static func delete<T: Decodable>(url: String, json: [String: Any]?, receiveModel: T.Type, completion:#escaping (Decodable) -> ())
{
send(url: url, json: nil, receiveModel: receiveModel, completion: completion, httpMethod: "DELETE")
}
private static func send<T: Decodable>(url: String, json: [String: Any]?, receiveModel: T.Type, completion:#escaping (Decodable) -> (), httpMethod: String)
{
// create post request
let urlURL: URL = URL(string: url)!
var httpRequest: URLRequest = URLRequest(url: urlURL)
httpRequest.httpMethod = httpMethod
if(json != nil)
{
// serialize map of strings to json object
let jsonData: Data = try! JSONSerialization.data(withJSONObject: json!)
// insert json data to the request
httpRequest.httpBody = jsonData
httpRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
}
// create an asynchronus task to post the request
let task = URLSession.shared.dataTask(with: httpRequest)
{ jsonData, response, error in
// on callback parse the json into the receiving model object
let receivedModelFilled: Decodable = Bundle.main.decode(receiveModel, from: jsonData!)
// cal the user callback with the constructed object from json
DispatchQueue.main.async {
completion(receivedModelFilled)
}
}
task.resume()
}
}
# save as: TestService.swift
import Foundation
struct TestService: Codable
{
let test: String
}
then you can use it like this:
let urlString: String = "http://localhost/testService" <--- replace with your actual service url
// call the API in post request
APICaller.post(url: urlString, json: ["test": "test"], receiveModel: TestService.self, completion: { testReponse in
// when response is received - do something with it in this callback
let testService: TestService = testReponse as! TestService
print("testService: \(testService)")
})
Tip:
i use online service to turn my JSONs into swift files, so all i have left is to write the call and handle the response
i use this one: https://app.quicktype.io but you can search for the one you prefer
Swift 5
Not exactly the same situation, but I was having similar problem. What finally helped me was this:
func myFunction(_ myType: AnyClass)
{
switch myType
{
case is MyCustomClass.Type:
//...
break
case is MyCustomClassTwo.Type:
//...
break
default: break
}
}
Then you can call it inside an instance of said class like this:
myFunction(type(of: self))
Hope this helps someone in my same situation.
Use obj-getclass:
CastDAO.invokeService("test", withParams: ["test" : "test"], returningClass: obj-getclass(self)) { cityInfo in /*...*/
}
Assuming self is a city info object.
I recently came across this looking for a way to make my UINavigationController invisible to everything but the subview buttons. I put this in a custom nav controller:
// MARK:- UINavigationBar Override
private extension UINavigationBar {
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Make the navigation bar ignore interactions unless with a subview button
return self.point(inside: point, with: event, type: UIButton.self)
}
}
// MARK:- Button finding hit test
private extension UIView {
func point<T: UIView>(inside point: CGPoint, with event: UIEvent?, type: T.Type) -> Bool {
guard self.bounds.contains(point) else { return false }
if subviews.contains(where: { $0.point(inside: convert(point, to: $0), with: event, type: type) }) {
return true
}
return self is T
}
}
Don't forget to use bounds instead of frame as point is converted before calling.