Object Factories in Swift

Objective-C makes it very easy to create a real object Factory. The dynamic nature of the language is very well suited for creating objects on the fly that are of unknown type at compile time. Swift is much more static type safe language, but that does not mean that the basic idea of a proper object factory cannot be achieved.

First, let’s clear something up. No proper object factory has types in if-else or switch on type blocks. That is not a proper factory. The point of a proper object factory is that it requires no additional code to create new types or remove old ones.

This is NOT how an object factory should look

    func createClass(string : String) -> BaseClass
        if string == "Subclass1"
            return Subclass1.init();
        if string == "Subclass2"
            return Subclass2.init()

Every time you add or a remove a class, you are changing the if or case statement to add or remove class names, which is highly error prone. In a language with header files, you will constantly inserting header files and creating dependencies.

So, with all that out of the way, let’s get started. Create a simple Single View iOS app, and create a new Swift class called Factory. It will have a type method called create(String) which will take a String parameter and return an optional of the proper subclass or nil if the String passed in is not valid. We will also create a simple method called hello which may or may not be overridden in newly defined subclasses. Also, since this is a pure Swift class, we will manually add a description() method, which will simply print out the full class name.

    1: class Factory
    2: {
    3:     class func create(name : String) -> Factory?
    4:     {
    5:         let appName = NSBundle.mainBundle().infoDictionary!["CFBundleName"] as! String
    6:         guard let any : AnyObject.Type = NSClassFromString(appName + "." + name) else
    7:         {
    8:             return nil;
    9:         }
   11:         guard let ns = any as? Factory.Type else
   12:         {
   13:             return nil;
   14:         }
   15:         return ns.init()
   16:     }
   18:     func description() -> String
   19:     {
   20:         return  NSStringFromClass(self.dynamicType)
   21:     }
   23:     required init()
   24:     {
   25:     }
   27:     func hello()
   28:     {
   29:         print("base hello");
   30:     }
   32: }

The first thing we do in the create(String) type method is create an object of AnyObject.Type. In Swift classes are prefixed by the application name, so we create a class using NSClassFromString with a combination of the application name from the bundle and the name of the class passed in.

The first guard statement on line 6, will check to see that the class name we passed in exists in the app. If it doesn’t we return nil, and are done. If it is a class in the app, we have to make sure that it is a derivative of Factory, hence the second guard statement at line 11. If we can cast to the one of the Factory subclasses we return an init()ed object.

We must create a default initializer in the base class because a metatype can only initialize its class by calling init(). Next, we will create two subclasses of Factory. These concrete classes will simply be called LargeConcrete and SmallConcrete

class SmallConcrete: Factory
    override func hello()
        print("derived hello");

class LargeConcrete: Factory

These classes don’t do much. SmallConcrete simply overrides the hello() method.

In our app delegate, in our didFinishLaunchingWithOptions, we have this code:

    1:         if let f = Factory.create("SmallConcrete")
    2:         {
    3:                 print(f.description());
    4:                 f.hello();
    5:         }
    6:         else
    7:         {
    8:                 print("No class")
    9:         }

This simply prints the name of the subclass created, and the result of the hello() method, or “no class” if the passed in String is either not a valid class in the app, or not a subclass of Factory. On line 1 if you change “SmallConcrete” to “LargeConcrete” the base class version of the hello() method will be called, which is what is expected and desired, since there is no override in the derived class.

Finally, in closing we can actually optimize the two guard statements and consolidate them into one statement. By doing this, we now know for certain that the cast to Factory.Type will succeed.

guard let any : AnyObject.Type = NSClassFromString(appName + "." + name) , let ns = any as? Factory.Type  else
    return nil;
return ns.init()

This code is valid as of beta 3 of Xcode 7.

Note: The above code does not work in a playground. Playgrounds can evaluate this line properly:

       let appName = NSBundle.mainBundle().infoDictionary!["CFBundleName"] as! String

but the classes do not use the appName as the prefix. Any attempt to get the metatype from the the combination of appName and the class name will always return nil. This is why the proper method for testing this is just to create a simple single view iOS application, so that the app name is properly prefixed to the class name.

Thoughts or comments are welcome : david@spanware.com