How to write a Custom Initialiser in Swift

I’m used to initialising my Objective-C objects with a custom initialiser. It’s a trick I’ve learnt from the legendary Simon Allardice back in the day. It works a treat every time!

Turns out it’s a Cocoa and Cocoa Touch design pattern, and the principle can be applied similarly in Swift, with a couple of important differences.

Or shall I say pitfalls?

Let’s take an NSDateFormatter for example. To declare it as a property in Objective-C, I’d do something like this:

@property (nonatomic, strong) NSDateFormatter *formatter;

// ...

- (NSDateFormatter *)formatter {
    
    if (!_formatter) {
        _formatter = [[NSDateFormatter alloc]init];
        _formatter.dateStyle = NSDateFormatterShortStyle;
        _formatter.timeStyle = NSDateFormatterNoStyle;
    }
    return _formatter;
}

Now my formatter is ready to use, initialised with a short date style and no time style. And a lot is happening under the hood: Xcode creates a variable (_formatter), and if anybody asks for my object, this method is called and checks if the variable exists. If it does, the variable is simply returned. If it does not exist, the method creates an alloc’d and init’d object for us. Everybody’s happy.

If later on I decide to change certain aspects of my property (for example, adding a time style), my property is updated.

This is lazy instantiation, which means that if nobody ever asks for my formatter, the object and memory are never called or allocated.

In Swift we have two options to accomplish more or less the same thing: declaring a global variable with a closure, or with the lazy keyword.

Using a closure (immutable)

Here’s how we can create our formatter in Swift with a closure:

var formatter: NSDateFormatter {
    let formatter = NSDateFormatter()
    formatter.dateStyle = .ShortStyle
    formatter.timeStyle = .NoStyle
    return formatter
}

Although this works just fine, the “variable” we’re creating is in reality a constant and hence cannot be changed later – even though the compiler forces us to create it with the var keyword: when we try to change an attribute of our formatter later, Xcode does not warn us that this is impossible. It just won’t work.

So if we need an immutable object, a closure is the way to go. But if we may want to mess with our object later, we’ll have to use a lazy initialiser.

Using the lazy keyword (mutable)

Here’s what our formatter looks like when we instantiate it using the lazy keyword:

lazy var formatter: NSDateFormatter = {
    let formatter = NSDateFormatter()
    formatter.dateStyle = .ShortStyle
    formatter.timeStyle = .NoStyle
    return formatter
}()

Apart from the equals sign, and the braces at the end, everything remains the same. This creates a true variable rather than a fake one without compiler pitfalls, and any attribute can later be changed.

One final note: just like in Objective-C, the code between the curly braces is only ever called once (if at all).

Note that this doesn’t work for all object types: trying to initialse a Swift Array for example will not work this way. If you encounter such trouble, simply write a function that returns the initialised values, like so:

    lazy var data: [String] = self.initData()
    
    func initData() -> [String] {
        
        // initialize data array and return it
        var data = [String]()
        
        let formatter = NSNumberFormatter()
        formatter.numberStyle = .SpellOutStyle
        
        for var i = 0; i < 30; i++ {
            let number = NSNumber.init(integer: i)
            data.append(formatter.stringFromNumber(number)!)
        }
        return data
    }

Differences to Objective-C

The point of lazy instantiation is that if we decide never to use our formatter, the object is never created. With a date formatter that’s not an issue, but if we wanted to do some additional configuration it may very well become one.

For example, let’s create a gesture recogniser with the same design pattern in a single view controller.

In Objective-C, we can add it to the view during the instantiation. Even though we never explicitly call the recogniser, it still ends up in our view:

@property (nonatomic, strong) UITapGestureRecognizer *tap;

// ..

- (UITapGestureRecognizer *)tap {
    
    if (!_tap) {
        _tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleTap)];
        [self.view addGestureRecognizer:_tap];
    }
    return _tap;
}

Trying to use this tap gesture results in success. However, doing the same thing in Swift leaves the object uncreated, and as a result we end up with no tap gesture in our view:

lazy var tap: UITapGestureRecognizer = {
    let tap = UITapGestureRecognizer.init(target: self, action: "handleTap")
    self.view.addGestureRecognizer(tap)
    return tap
}()

The line self.view.addGestureRecognizer(tap) is ignored, because it’s never called. Why this happens I can’t tell you, neither do I know why this works in Objective-C.

All I can tell you is that to add the recogniser in Swift, we need to call SOMETHING on this variable: print it as a log message, ask for its view, anything to make it spring into life:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // print a log message
    print(tap)
    
    // ask for its view
    tap.view
    
    // or - probably best - add it to the view here
    self.view.addGestureRecognizer(tap)
}

If you choose Option 3 (the most sensible), there’s obviously no need to add the gesture in the custom initialiser of course.

Conclusion

The big difference between these approaches is that the lazy Swift initialisation method is not as contained as it is in Objective-C, where we can configure the whole object. That’s especially important when it comes to adding things to an instantiated view controller.

In Swift we have to write the initialiser, and then manually trigger the method to use the object by other means later. viewDidLoad is a good option (if we have it), otherwise perhaps awakeFromFetch or anything similar that is called when your class comes to life.

Further Reading





Leave a Reply