Calling methods from strings in Swift

Written by Emil Loer on Feb 24, 2016 |

In this article I want to explore the various ways you can invoke a method when the only thing you have is a string containing the method's name. This can be useful when you have to decide at run-time which method to call. An example where I use this is to select the proper image method in a PaintCode StyleKit from an IBInspectable value.

Note that Swift currently only supports dynamic method invocation on objects deriving from NSObject.

Selectors from strings

The first thing we have to do is to obtain a selector from our string. A selector is basically the runtime's way of describing the name of a method. In Swift selectors are represented by the Selector type.

Because Selector adopts the StringLiteralConvertible protocol converting a string to a selector is real easy:

let selector: Selector = "addSubview:"

This however doesn't help us if we want to provide the selector from something that isn't a string literal, e.g. a string in a variable. For this we can use NSSelectorFromString:

let name = "addSubview:"
let selector = NSSelectorFromString(name)

PerformSelector

Now that we have a selector we can use it to invoke a method. For this we can use the performSelector: method family of NSObject. This method comes in three variants:

func performSelector(_ aSelector: Selector) -> Unmanaged<AnyObject>!

func performSelector(_ aSelector: Selector, withObject object: AnyObject!) -> 
    Unmanaged<AnyObject>!

func performSelector(_ aSelector: Selector, withObject object1: AnyObject!,
    withObject object2: AnyObject!) -> Unmanaged<AnyObject>!

The first version can be used to call a method expecting zero arguments and returning a object. The second version expects a single object argument and the third version expects two object arguments.

Note the use of an Unmanaged return type. When you dynamically invoke a method the compiler can't infer if the return value should be interpreted as a +1 or +0 reference. For more information about unmanaged references see this article on NSHipster.

Non-object arguments

There is a big gotcha with performSelector: and friends: it can only be used with arguments and return values that are objects! So if you have something that takes e.g. a boolean you're out of luck.

An experienced Objective-C programmer in the room would now stand up and say: "No sweat! Just use NSInvocation instead!". But try this and you get the following line of disappointment:

error: 'NSInvocation' is unavailable in Swift: NSInvocation and related APIs not available

Runtime trickery

Leveraging the power of the Objective-C runtime we can still make this work. For this example I am going to assume the method we are calling has the following signature: Bool -> UIImage, but it can be anything as long as it isn't variadic.

The first thing we have to do is do a lookup on the object for the required method. For this we can use the class_getInstanceMethod and class_getClassMethod, depending on the type of method we are looking for.

If we have an owner of type AnyObject then we can use the fact that AnyClass conforms to AnyObject to detect if we have to lookup a class or instance method:

let method: Method
if owner is AnyClass {
    method = class_getClassMethod(owner as! AnyClass, selector)
} else {
    method = class_getInstanceMethod(owner.dynamicType, selector)
}

Both functions take a class as a first argument, so in the case of an instance method we have to dynamically infer its type first. The resulting method may be nil, so don't forget to check for that with a guard method != nil clause or things will behave unexpectedly.

The Method type is an opaque struct, so the next step is to get something useful out of this. We can use method_getImplementation to get a value of type IMP. In Swift this IMP type is an opaque pointer, so not very useful. But in Objective-C this type is actually a function pointer to the method's implementation. It has two additional (hidden) arguments prefixed: a reference to self and the selector.

This means that our example Bool -> UIImage function can be mapped onto the implementation function pointer in the following way:

typealias Function = @convention(c) (AnyObject, Selector, Bool) -> Unmanaged<UIImage>

Note that here we also have to use unmanaged return values so we can manually tell Swift how to handle the reference counts.

We can obtain our method by bit casting the implementation to our function pointer type.

let implementation = method_getImplementation(method)
let function = unsafeBitCast(implementation, Function.self)

Below is the combined code for a function that takes an owner and a selector and returns a method that can be called. For ease of use the owner and selector are curried into the method implementation and the unmanaged result is interpreted as a +0 reference. The function returns nil if the method wasn't found.

func extractMethodFrom(owner: AnyObject, selector: Selector) -> (Bool -> UIImage)? {
    let method: Method
    if owner is AnyClass {
        method = class_getClassMethod(owner as! AnyClass, selector)
    } else {
        method = class_getInstanceMethod(owner.dynamicType, selector)
    }

    guard method != nil else {
        return nil
    }

    let implementation = method_getImplementation(method)

    typealias Function = @convention(c) (AnyObject, Selector, Bool) -> Unmanaged<UIImage>
    let function = unsafeBitCast(implementation, Function.self)

    return { bool in function(owner, selector, bool).takeUnretainedValue() }
}

How to use

To use this as-is you can do something like this:

let name = "imageWithBorder:" // e.g. from somewhere else
if let method = extractMethodFrom(imageGenerator, name) {
    let image = method(true)
    // Use image here
}

Of course you can freely modify the extractMethodFrom:selector: function to match any method signature you need. You can use any number of arguments and any kind of return value, just as long as you make sure that you treat the return value as unmanaged if it is an object. Optionals work fine too.

In the next post I will go more into details about how I use this technique to make the integration between UIKit and PaintCode more seamless.

In the meantime, if you've enjoyed this post please follow me on Twitter or Facebook. I'd really appreciate it!

ChordFinder 1.4.0 released

Written by Emil Loer on Feb 17, 2016 |

Today I'm announcing the release of ChordFinder version 1.4.0. This is a compatibility update bringing better support for OS X El Capitan.

Go get it now!

Collecting app feedback with Codebase issues

Written by Emil Loer on Feb 15, 2016 |

An important part of app development is listening to your users and learn what they think of your app. There are a couple of methods you can do this. You can wait until people review your app in the App Store, but without an incentive they will most likely only do this when something is wrong. The same goes for contact information on your website. Of course you can instruct your users to go and review your app and leave suggestions or send some e-mails but this requires the user to leave the app. Not a great experience, I think, because of the many steps involved.

There is a better way. For an app I'm currently developing I'm building a feedback form inside the app. This allows me to ask my users what they think of the app while staying in the app. Furthermore, I can ask them the exact questions that I need to know to help them.

To accomplish this I'm trying to make use of what I already have. In this case, I will use the ticketing system inside Codebase because I already use it for many other things.

Getting access

Codebase uses an XML over HTTP API that you can leverage for your own projects. In order to gain access and create tickets you need to have a project, a user and the user's API credentials. You can find the API credentials for the current user under the "My Profile" section of Codebase. For the project you need the permalink name, this can be found in the project settings and is usually the project name in lowercase with spaces replaced by hyphens.

Let's put all of these in some constants first:

let project = "your-codebase-project"
let user = "mycompany/myusername"
let apiKey = "your Codebase API key here"

Collecting information

In order to create a ticket we need to tell Codebase what is actually in the ticket. In the most simple case a ticket consists of a summary and a description. The summary can be interpreted as the ticket's title so that's how we will use it here. Let's put this inside a Swift struct:

/// A Codebase feedback ticket
struct CodebaseTicket {
    /// The ticket's summary (title). 
    var summary: String

    /// The ticket's description string. This can consist of multiple lines.
    var description: String
}

Now because Codebase uses XML over HTTP we have to convert this ticket to an XML representation. If you're on Mac OS X you can use NSXMLDocument for this, but on iOS this class is not available so we'll have to resort back to string concatenation. But I'll give you both options!

#if os(OSX)
    // Construct an XML document for the ticket
    let ticket = NSXMLElement(name: "ticket")
    ticket.addChild(NSXMLElement(name: "summary", stringValue: summary))
    ticket.addChild(NSXMLElement(name: "description", stringValue: description))

    let body = NSXMLDocument(rootElement: ticket).XMLData
#else
    let body = "<ticket>" +
        "<summary><![CDATA[\(summary)]]></summary>" +
        "<description><![CDATA[\(description)]]></description>" +
        "</ticket>".dataUsingEncoding(NSUTF8StringEncoding)
#endif

Now body contains an NSData containing the XML document, which will be used in the next section.

Submitting the ticket

For submitting the ticket to Codebase we can use NSURLSession. There is a catch however, because Codebase wants us to authenticate to the API using HTTP Basic Authentication. This normally consists of two steps: waiting for a WWW-Authenticate header from the server and sending a second request using an Authorization header. Unfortunately, waiting for the authenticate header using NSURLSession requires setting up a delegate and writing a lot of boilerplate code. But there is a simpler way: we can just set the authorization header directly in our URL request.

Here's how to do it:

// Create the URL request
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
request.HTTPMethod = "POST"
request.HTTPBody = body
request.addValue("application/xml", forHTTPHeaderField: "Accept")
request.addValue("application/xml", forHTTPHeaderField: "Content-Type")

// Set the authorization header
let credentials = "\(user):\(apiKey)"
let auth = "Basic " + credentials.dataUsingEncoding(NSUTF8StringEncoding)!.base64EncodedStringWithOptions([])
request.addValue(auth, forHTTPHeaderField: "Authorization")

Now we just have to issue the request and do something with the result inside the request callback.

// Issue the request
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
    // Do something with the result here
}

task.resume()

Using the tickets

The only thing that remains is actually using the ticket struct and integrating it in your app. The code below can be used in your favourite view controller.

let ticket = CodebaseTicket(
    summary: "My feedback",
    description: "I think your app is awesome!"
)

ticket.postWithCallback { error in
    if let error = error {
        // Handle the error
    } else {
        // Everything went ok
    }
}

Complete code example

The complete code for the Codebase ticket creator is given below.

import Foundation

/// A Codebase feedback ticket
struct CodebaseTicket {
    let project = "your-codebase-project"
    let user = "mycompany/myusername"
    let apiKey = "your Codebase API key here"

    /// The ticket's summary (title). 
    var summary: String

    /// The ticket's description string. This can consist of multiple lines.
    var description: String

    /// Send the ticket to Codebase. Calls the callback on completion with an
    /// NSError instance when something went wrong, nil otherwise.
    func postWithCallback(callback: NSError? -> Void) {
        let url = "https://api3.codebasehq.com/\(project)/tickets"

        #if os(OSX)
            // Construct an XML document for the ticket
            let ticket = NSXMLElement(name: "ticket")
            ticket.addChild(NSXMLElement(name: "summary", stringValue: summary))
            ticket.addChild(NSXMLElement(name: "description", stringValue: description))

            let body = NSXMLDocument(rootElement: ticket).XMLData
        #else
            let body = "<ticket>" +
                "<summary><![CDATA[\(summary)]]></summary>" +
                "<description><![CDATA[\(description)]]></description>" +
                "</ticket>".dataUsingEncoding(NSUTF8StringEncoding)
        #endif

        // Create the URL request
        let request = NSMutableURLRequest(URL: NSURL(string: url)!)
        request.HTTPMethod = "POST"
        request.HTTPBody = body
        request.addValue("application/xml", forHTTPHeaderField: "Accept")
        request.addValue("application/xml", forHTTPHeaderField: "Content-Type")
        // Set the authorization header
        let credentials = "\(user):\(apiKey)"
        let auth = "Basic " + credentials.dataUsingEncoding(NSUTF8StringEncoding)!.base64EncodedStringWithOptions([])
        request.addValue(auth, forHTTPHeaderField: "Authorization")

        // Issue the request
        let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
            callback(error)
        }

        task.resume()
    }
}

Final thoughts

Of course this is just the beginning. Codebase understands many more properties like priorities, categories and tags. The framework I provided can easily be extended to supply whatever information you need to the API. For more information about the possibilities see the Codebase API documentation.

If you've enjoyed this post please follow me on Twitter or Facebook. I'd really appreciate it!

...And We're Off!

Written by Emil Loer on Feb 11, 2016 |

Hooray! Today marks yet another milestone in the history of my little app company because I'm launching a brand new web site. The old site had some hosting issues and was cumbersome to maintain, so I decided to start from scratch by leveraging the recently announced Lektor engine. And I must say, it's a really powerful system and still very lightweight. Makes making web sites somewhat fun again.

So, what can you expect here from now on? Better pages for all of my apps, more frequent blog posting and reduced page loading times. In the next few weeks I plan to add a lot more content to my app pages with screenshots and all that fancy jazz, and I'll post regular updates on upcoming apps, so stay tuned for that. If I feel like it I might even write some technical articles about Swift and such!

Now it's back to work for me. I have a big new app that I'm working on for a completely new target audience, so that's pretty exciting. More on this later... ;)

In the meantime, don't hesitate to follow me on Twitter or Facebook.

« Previous | 2 | Next »