169

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?
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
Jean-Francois Gagnon
  • 3,141
  • 4
  • 20
  • 27
  • I dont think that Swifts generics work like javas. thus the inferrer cannot be that intelligent. id omit the Class Thing and specify the Generic Type explicitely `CastDAO.invokeService("test", withParams: ["test" : "test"]) { (ci:CityInfo) in }` – Christian Dietrich Jun 19 '14 at 14:59
  • i have been in your situation and everyone wrote the same - it cant be done, BUT if you take all the answers written here you can achieve just that, see my answer: https://stackoverflow.com/a/68930681/530884 – Shaybc Aug 25 '21 at 22:58

7 Answers7

156

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.

EliaCereda
  • 2,410
  • 1
  • 17
  • 24
  • Thanks for the T.Type hint - just what I needed for passing a class type as an argument. – Echelon Jan 28 '16 at 11:53
  • Instead of using a templated function, it's also possible to pass the returningClass as an Initiable.Type – cyril94440 Mar 12 '18 at 10:01
  • In the original code that would’ve produced different results. The generic parameter `T` is used to express the relation between the `returningClass` and the object passed to `completionHandler`. If `Initiable.Type` is used this relationship is lost. – EliaCereda Mar 20 '18 at 10:08
  • AND? Generics don't allow to write `func somefunc()` – Gargo Mar 28 '18 at 18:04
  • Gargo, what are you referring to? – EliaCereda Mar 29 '18 at 07:22
  • Thank you so much. Excellent answer: 1) I was getting a different error. **Cannot invoke 'returningClass' with no arguments**. Doing the `` solved it. 2) * it is often useful to move every argument of the method call to its own variable.* not sure what your suggestion means. What's the original way of doing it that doesn't follow your tip? – mfaani Apr 05 '18 at 17:00
  • @Honey, you’re welcome. The tip was to split an expression in multiple lines to pinpoint the exact source of an error. Bear in mind that my answer was written back in the Swift 1.x era and I don’t think I ever revisited it with more recent compilers. Error diagnostics were a big source of pain at the time, especially when dealing with complex generic expressions. The situation greatly improved over the years, so I expect that current diagnostics would be much clearer, making that tip somewhat superfluous. The answer could use some clarification on this part. – EliaCereda Apr 05 '18 at 21:54
39

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.

holex
  • 23,961
  • 7
  • 62
  • 76
  • 1
    you can also do `self.dynamicType` – newacct Jul 14 '14 at 18:36
  • 1
    Whenever I try to use myClass it's causing an error of " use of undeclared type "myClass". Swift 3, latest builds of Xcode and iOS – Confused Dec 12 '16 at 04:13
  • @Confused, I see no such issue with that, you may need to give me more info about context. – holex Dec 12 '16 at 09:09
  • I'm making a button. In that button's code (it's SpriteKit, so inside touchesBegan) I want the button to call a Class function, so need a reference to that class. So in the Button Class I have created a variable to hold a reference to the Class. But I can't get it to hold a Class, or the Type that is that Class, whatever the right wording/terminology is. I can only get it to store references to instances. Preferably, I'd like to pass a reference to the Class Type into the button. But I just started with creating an internal reference, and couldn't even get that working. – Confused Dec 12 '16 at 09:26
  • It was a continuation on methodology and thinking I was using here: http://stackoverflow.com/questions/41092440/spritekit-start-button-that-calls-start-in-the-scene-its-in. I've since gotten some logic working with Protocols, but fast coming up against the fact I'm also going to need to figure out associated types and generics to get what I thought I could do with simpler mechanisms. Or just copy and paste a lot of code around. Which is what I normally do ;) – Confused Dec 12 '16 at 09:28
  • @Confused, you may need to extend your question by some code on your post... could you do that, please? – holex Dec 12 '16 at 09:32
  • Yeah, that question was written before I thought about trying to pass a class type... when I was just trying to get at the scene in which a button was. Still haven't gotten that done without protocols. – Confused Dec 12 '16 at 09:33
  • Some of my ridiculous efforts added to the question linked above... laugh at me. Mercilessly. – Confused Dec 12 '16 at 09:44
  • @Confused, I will not laugh at you. :) – holex Dec 12 '16 at 09:45
  • You must. I insist. It will be the only way to tolerate my rampant stupidity and the fact that I get things, forget things, and then struggle to re-get them, before forgetting them, again and again and again... – Confused Dec 12 '16 at 09:51
14

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
    }

}

hackzfy
  • 141
  • 1
  • 2
  • I'm trying to do something similar, but I get an error at the callsite: `Static method … requires that 'MyDecodable.Type' conform to 'Decodable'`. Would you mind updating your answer to give an example call to `loadItem`? – Barry Jones Feb 22 '20 at 18:28
  • Turns out this is the point where I have to learn the difference between `.Type` and `.self` (type and metatype). – Barry Jones Feb 22 '20 at 18:55
4

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

Shaybc
  • 2,628
  • 29
  • 43
3

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.

Merricat
  • 2,583
  • 1
  • 19
  • 27
2

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.

PSchuette
  • 4,463
  • 3
  • 19
  • 21
1

Use obj-getclass:

CastDAO.invokeService("test", withParams: ["test" : "test"], returningClass: obj-getclass(self)) { cityInfo in /*...*/

}

Assuming self is a city info object.

Undo
  • 25,519
  • 37
  • 106
  • 129