A simple solution in Swift to reuse UIDatePicker with storyboards

Korey Hinton —> Blog —> A simple solution in Swift to reuse UIDatePicker with storyboards

So here's the problem

../media/datepicker.png

The problem at hand

Applications that allow users to assign dates usually display date pickers in multiple places throughout the app and the kind of date picker will differ based on what kind of date they are picking from. In one case they might be picking a time only, another time they might picking a date, and yet in another place they might need to pick both. It would be nice to be able to reuse a single date picker while also being able to customize it as needed. It is important that reusing it is very simple especially since the date picker control can be used over and over again in an application that makes use of dates.

Date Pickers displayed in isolation

Date pickers are used in various contexts. We see them in popovers, action sheets, presented views, or embedded in other content. We are going to focus on reusing the date picker whenever it is displayed in isolation. So anytime we segue to a view controller that only contains a date picker.

What problem we aren't solving

We won't be addressing cases where date pickers are contained as subviews. We are solving the problem of easily reusing it when it is in its own view controller. Of course view containment will work if you add a container view and embed the view controller with the date picker inside that container. View containment will allow you to reuse the same date picker solution I am presenting to you in this article.

No built-in solution

Before I actually created a solution to this problem I actually wanted to see if there was a way to do this without wrapping UIDatePicker. What I wanted to do was drag a value changed IBAction to the presenter view controller. Obviously this isn't allowed since it can only be an IBAction to its own view controller. Wouldn't it be nice if Apple provided some kind of control that could be dragged out and hooked into the presenter view controller some how. As far as I could find there isn't an easy solution like that readily available.

We have to wrap UIDatePicker with a UIViewController subclass

The obvious solution would be to create a wrapper class that will allow us to customize the presented date picker.

How to construct the wrapper's interface

So if you are thinking delegation, it does look like a good solution at first glance but let's dig deeper.

Does delegation fit the bill?

Delegation would allow us to notify the delegate of important things like the date changed in the date picker or the date picker is ready to be customized.

Why not delegation?

So if delegation seems to work why not use it. The problem is that it creates a lot of overhead for something so simple. Every view controller in the app that wants to present the date picker will have to conform to the protocol, create delegate methods and save local state needed in those methods as member variables, and will have to remember to set themselves as the delegate. When this has to be done over and over again it is likely you will search for a more ideal solution that makes reuse easy. Luckily I know of one.

Completion block/closure APIs are easier to implement over and over again

The nice thing about completion blocks in Objective-C and closures in Swift are that the local state can be maintained since it can have inlined functions that can maintain that state. Also, this allows copying and pasting or reading the code to re-implement somewhere else a piece of cake. In the case of delegates I have to look in multiples places: where the delegate is set, where the presented class is customized, and where the delegate methods are implemented.

A reusable solution

For our solution we are going to subclass UIViewController containing the date picker. We'll call it DatePickerController. This will be the reusable component.

Save yourself some headache and use closures

Ok, so now we have a DatePickerController class that is a subclass of UIViewController and wraps the UIDatePicker. One valid approach would be to use delegation so we can inform our delegate when important things happen, like the value changed (the date changed) on the date picker. The problem with this is that every new place we want to add a segue to this date picker wrapper we are going to have to conform to the protocol, implement all required protocol methods and any optional ones we want, and then make sure we assign the delegate. While this doesn't sound like a ton of overhead it really gets to be a pain once you do it several times. What would be ideal, is once we get a reference to the DatePickerController we could set a completion closure for important things like value changed or view loaded. This allows us to set-up everything in the prepareForSegue method and makes it extremely easy to reuse with minimum amount of coupling.

Preparing for segues

Wouldn't it be nice to be able to put all DatePickerController interaction in one place, prepareForSegue? Imagine how easy it is to find all of this code when we need to break out another place that will present the date picker.

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let datePickerController : DatePickerController = segue.destinationViewController as? DatePickerController {
        datePickerController.valueChangedCallback = {(date)->Void in
            self.date = date
            self.dateLabel.text = self.dateFormatter.stringFromDate(date)
        };
    }
}
View loaded

Part of the reason using a date picker from the storyboard gets tricky is because you can't set any of the date picker properties until viewDidLoad has been called, yet the place where we get a reference to the DatePickerController is in prepareForSegue from the presenting class, which at that time the view is not loaded yet. So let's add a completion closure variable. This will nicely allow them to set the completion inline without having to implement a separate callback method.

var viewLoadedCompletion : ((datePicker: UIDatePicker)->Void)!

And then the DatePickerController will have to call the completion closure function if it has been set in viewDidLoad.

override func viewDidLoad() {
    super.viewDidLoad()

    if (self.viewLoadedCompletion != nil) {
        self.viewLoadedCompletion(datePicker: self.datePicker)
    }
}

Value changed event

We'll have the DatePickerController conveniently handle the value changed event of the date picker by connecting it as an IBAction from the storyboard. Then we call the completion closure if it has been set.

var valueChangedCompletion : ((date: NSDate)->Void)!

@IBAction func valueChanged(sender: UIDatePicker) {
    if (self.valueChangedCompletion != nil) {
        self.valueChangedCompletion(date: sender.date)
    }
}

Putting it together

class DatePickerController: UIViewController {

    @IBOutlet var datePicker: UIDatePicker!

    var viewLoadedCompletion : ((datePicker: UIDatePicker)->Void)!
    var valueChangedCompletion : ((date: NSDate)->Void)!

    override func viewDidLoad() {
        super.viewDidLoad()

        if (self.viewLoadedCompletion != nil) {
            self.viewLoadedCompletion(datePicker: self.datePicker)
        }
    }

    @IBAction func valueChanged(sender: UIDatePicker) {
        if (self.valueChangedCompletion != nil) {
            self.valueChangedCompletion(date: sender.date)
        }
    }
}

Designated responsibilities

While this is a very lightweight solution for now, as we need to expand its functionality it is important to realize what functionality belongs to the DatePickerController and what functionality belongs to the caller.

The wrapper

The wrapper is meant to be a general-purpose solution to date and time picking and is meant to be acted on. Code that sets up what kind of date picker might be tempting to put in the wrapper if you are using just a certain type of date picker but if you ever need it to do time instead of dates then you are going to have to refactor or create a different a different controller in the storyboard and a new class to wrap the date picker.

Public IBOutlet

The UIDatePicker object is exposed publicly (which is unavoidable in Swift) in the wrapper so that the caller/presenter can customize it as they want when they want.

Value changed IBAction

The IBAction is meant to pass important date updates to the caller. Unless you are adding a custom feature to all of your date pickers in the control itself (like displaying a date) then you won't likely need to do anything in the wrapper other than call the completion closure set by the caller.

The presenter

The presenter is responsible for displaying the picker, setting it's initial display date, and responding to the date/time updates that happen to the picker.

Date Picker customization

The presenter can customize the date picker once the date picker's view has loaded. Any customization before then will be overrided by the storyboard. For this reason we have the viewLoadedCompletion closure.

Setting the date

Remember you may or may not want to start the date picker's display on the current date. For this reason, the date picker is passed to you in the completion block of viewLoadedCompletion.

Updating display

Each presenter instance will be responsible for updating its own display. If you have repeating code over and over again you may consider finding a way to reuse the update display portion as well, but it will likely be nothing more than updating a label's text and updating the model with the newly selected date.

Implementing multiple date pickers

Implementing multiple date pickers couldn't be easier. There is a tiny bit of storyboard work to do and then the implementation code is easy.

Same storyboard

So this is the ideal scenario. If all of your view controllers in your app are in 1 single storyboard then reuse is so easy. All controllers that need our date picker wrapper can easily set up segues to it.

Multiple storyboards

With multiple storyboards it is a little bit harder, but not really. Just copy paste the view controller. Make sure it is still hooked up with your DatePickerController class and its date picker outlet and then you are done.

Implementing class code

Here is an example that switches between time picker in popover and date picker in a show segue.

import UIKit

class ViewController: UIViewController {

    let dateFormatter : NSDateFormatter = NSDateFormatter()
    var date : NSDate = NSDate()

    @IBOutlet var dateLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        self.dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
        self.dateLabel.text = self.dateFormatter.stringFromDate(self.date)
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

        let isPopover = segue.identifier == "popover"

        if let datePickerController : DatePickerController = segue.destinationViewController as? DatePickerController {


            datePickerController.valueChangedCallback = {(date)->Void in
                self.date = date
                self.dateLabel.text = self.dateFormatter.stringFromDate(date)
            };

            datePickerController.viewLoadedCallback = {(datePicker)->Void in
                if isPopover {
                    self.dateFormatter.dateStyle = NSDateFormatterStyle.NoStyle
                    self.dateFormatter.timeStyle = NSDateFormatterStyle.ShortStyle
                    datePicker.datePickerMode = UIDatePickerMode.Time
                } else {
                    self.dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
                    self.dateFormatter.timeStyle = NSDateFormatterStyle.NoStyle
                    datePicker.datePickerMode = UIDatePickerMode.Date
                }
                datePicker.date = self.date
            };
        }
    }
}

Okay so show me one without so much logic

Look how simple this is:

import UIKit

class ViewController: UIViewController {

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

        if let datePickerController : DatePickerController = segue.destinationViewController as? DatePickerController {

            datePickerController.valueChangedCallback = {(date)->Void in
                // respond to date changes
            };

            datePickerController.viewLoadedCallback = {(datePicker)->Void in
                // customize the date picker now that its view is loaded
            };
        }
    }
}

Conclusion

While it might seem that I am obsessed with completion blocks or closures, they aren't always the answer. But if reuse becomes easier as a result, then yes I do think they should be the answer.

Importance of reuse

Code reuse has been a topic that seems to keep coming up in my work over and over again. Its probably because I keep seeing repeated code that doesn't need to be repeated. Its important to realize that the best way to make code reusable is by repeating yourself first and then refactoring. Had I tried to do it the other way around I would have probably solved the wrong problem.

Future solutions to look for

Keep an eye out for a better way to do this. A lot of times something you write to workaround lack of framework code solving a problem may eventually have a solution in the future.

Date: 2014-12-01T15:34+0000

Author: Korey Hinton

Org version 7.9.3f with Emacs version 24

Validate XHTML 1.0