Towards Better Split View Controllers (I): Our Custom Split Controller - Digital Leaves
1429
post-template-default,single,single-post,postid-1429,single-format-standard,qode-social-login-1.0,qode-restaurant-1.0,ajax_fade,page_not_loaded,,select-theme-ver-4.1,wpb-js-composer js-comp-ver-5.2,vc_responsive

Towards Better Split View Controllers (I): Our Custom Split Controller

UISplitViewController is one of the most basic controllers in iOS, and can be found in many Apple applications. However, when you have a look at 3rd party applications, specially those that mind the details, you will notice that it’s not a very popular choice. Why is that? Well, there are several reasons. First of all, its appearance configuration options are really limited, what you see is what you get. You cannot really mess with the structure of the master-detail frame, their sizes, the segue animations and a lot of other things without hacking around with it, but the most important limitation of UISplitViewController is the fact that all detail screens have to share the same format and structure.

This last restriction is specially important if you want to use the split view controller for displaying different contextual pieces of information. Let’s say that you have an entity like a user with personal information, job position, location, associated credentials, etc, and you might want to use different screens for that, showing a map in the location screen, a table for the personal information, and some textfield for changing the password in the credentials screen. UISplitViewController wouldn’t be a good fit for that, because all detail screens should have the same structure, only different data.

In these scenarios, we need to develop a custom split view controller to meet our needs. In this post, we are going to do exactly that.

Our application

Our application will manage a list of users, and, when clicking on a user, it will show a custom split view controller with three sections:

  • A screen with details about the user, including the name, email, birth date, etc. This screen will allow editing the information fields for the user (by segueing to a new screen).
  • A screen showing a map with the location of the user
  • A screen with the credentials, including the userID, and a field to change the user’s password.

Our model

The model for our application is just a list of user. We will use a singleton class called ModelManager to generate a list of users (I used random data from https://randomuser.me and images from unsplash.com) and retrieve them when needed. We’ll also define a method to update our model when a person is modified.

Basic view structure

We’ll begin with a single view application. Our initial view controller will just show a list of current users in an standard UITableView. We will create a new UIViewController and call it MainViewController. This view controller will be our custom split controller. We’ll add a UITableView at the left acting as the “master” view controller in an UISplitViewController, pinned to the top-left-bottom and with 1/3 of the width of the parent view. We will cover the rest of the view with a “Container View”, that will act as the manager for our different detail view controllers. The container view will add the embedded UIViewController, and we will create a new UIViewController subclass for it called ContainerViewController, as it will contain our different detail view controllers.

The custom, empty segue

The key for switching between the different detail view controllers is creating a custom, empty segue subclass that will do nothing by default. This segue will allow us to “replace” one view controller with another in the context of the container view controller. We will thus create a subclass of UIStoryboardSegue called EmptySegue with an empty “perform” method:

Now we will start adding our detail view controllers. We will add three of them:

  • PersonalInformationViewController: a view controller in charge of displaying (and later modifying) the information of a concrete user. This view controller will have a UIImageView and a label for the avatar and name of the usr, and then a table of fields for the user that we will be able to modify later.
  • LocationViewController: will show the location of the user in a map, and the address just below in a label.
  • CredentialsViewController:  will show the user ID and allow the user to change the password via the usual “current password”/”new password” textfields combo.

We will add the appropriate controls to that view controllers, set de data sources and delegates, and then add the segues from the container view controller to our contained view controllers. When adding the segue with the usual Ctrl+Click&Drag iteration, we will be presented with the usual segue types plus our “empty” type. We need to select this last type and then go to the attributes inspector for the segue and make sure that the Class of the segue is set to “EmptySegue” (our custom segue subclass) and that the Module is set to “CustomSplitControl” (or the name of our current project).

If we don’t specify the module, Xcode will usually crash when segueing to the destination view controller showing an error that looks like this:

So make sure to specify the custom class and module properly. Ok, with all the IB stuff ready, it’s time to start coding!

Our main view controller

Our MainViewController will contain the “master” table with the rows that will trigger the details view controllers. As such, the table view has to access the container view controller contained in the Container View. To do so, we will make use of the fact that, when loaded, the container view will generate a segue to the container view controller in our main view controller. Thus, we will create a weak variable to hold the container view controller and initialise it in the prepare(forSegue) method when this segue occurs:

Notice how we have this “person” property, that will represent the person of user we are currently selected. This property, that will be transmitted at the segue from the initial view controller, will be transmitted also to the container view controller via the prepare(forSegue) method too, to give access to it to the ContainerViewController.

The ContainerToMenuDelegate will allow the container view controller to communicate back to the main view controller, via a set of defined methods that we can specify in the protocol. This way, we have the two-way communication covered between our “master”  and “container” views.

Now we will define the UITableView data source and delegate methods for displaying the different options, corresponding to the three view controllers that we mentioned above. We will define a row for the first one containing the picture of the user alongside some information, and a single option name cell for the rest of options.

Thus, we will modify our main view controller to handle the UITableView data and interactions:

Whoa! That’s a whole lot of code. Don’t panic, let’s go through it step-by-step:

  1. These are our “master” table view options for triggering the detail view controllers. We’ll make a distinction between the first option and the next ones because we need to take care of the automatic selection of the first row to trigger the first segue in the container view controller automatically when the view controller is shown, thus showing the initial detail view controller. We will be careful to name the options after the names of the segues in the IB.
  2. As we want to show the first contained view controller automatically upon showing the view, we need to select the first row automatically. This is done thanks to the tableView:willDisplay:forRowAt:. We will set a variable isLoadingTable that will indicate whether we have already selected the first row or not. If not, we will make sure we are about to display the first row and then select it automatically.
  3. In viewDidLoad, we will add a fancy shadow to the right edge of the master table view (the table view must be on top of the container view in the hierarchy or the shadow won’t be visible), set the automatic dimension for our rows (so the cells will adapt to the content of the different rows), and register our custom cell nibs for the table view.
  4. If some of our detail views contained in the container view controller modify the data for the user, we will need to refresh our master table view (as it’s showing some info for the user). Thus, in the ContainerToMenuDelegate implementation, we will update our person with the modified person object, and reload the data for the table.
  5. The data source implementation is pretty straightforward. We just have the main option plus the secondary ones for rows, and depending on the indexPath we will display the initial, information details row or just the basic option name row.
  6. We will add an empty footer just to avoid iOS showing “empty” rows at the bottom of our valid rows.
  7. When the user selects a row, we need to indicate our Container View Controller to change the contained view controller (switch to another detail VC). We will define a method in ContainerViewController called setContainedViewController, and call it in didSelectRow.

Now let’s take care of our ContainerViewController.

Our container view controller

Our container view controller will manage the different contained view controllers.

You will remember that when a row was selected in our main view controller options table view, we will call the container view controller’s setContainedViewController method. Let’s have a look at how it works. The method basically selects the desired option and performs a segue to that option. Then, in the prepareForSegue method we will differentiate three cases:

  1. If we are selecting the first menu option, we might need to address the situation in which no previous view controller has been selected before. Thus the need of having this options separated from the array of secondary options. If the option is the main one, and we have no current child view controllers (childViewControllers.count == 0), then we need to add the initial view controller, that will be our PersonalInformationViewController. We will do that by setting the destination frame to our current container view controller frame and moving the view controller and the view to our current view/view controller hierarchy as you would usually do. If we have some child view controller already (childViewControllers.count > 0), this is the “normal” scenario in which we are moving from another contained view controller back to the first one, so we will act as in the general case (2, see below).
  2. This is the general case, in which we are moving from a contained view controller (from) to the desired new view controller (to) by selecting it from the list of options in the “master” menu. We will call the “swapFrom” method that does exactly that, making sure to remove the “old” view controller and adding the “new one” to the hierarchy.
  3. The third case happens when we want our detail view controllers to trigger secondary segues constrained to the container view controller scope. Imagine that you want to transition from a detail view controller to a new view controller via a modal segue, and then back to the detail view controller. We will use what we call a secondary segue for that. We will use this case to edit a field from the user’s information screen later.

Notice how we defined different transition types in swapFrom to transition between our view controllers. This is another advantage of our custom split view controller: we have full control on how the detail views are presented.

Now, in order to establish a communication channel between the container view controller and the different contained view controllers to be shown, we will use two mechanisms:

Content view controllers custom subclass

We will make our content view controllers to inherit from a subclass of UIViewController that we will call CustomSplitContentViewController. This subclass will have a “person” variable, a “parameters” property and a delegate pointing back to the container view controller:

  1. The person variable will be loaded with the current person that we are handling, back from the main view controller. This will allow the content view controller to get the information from the person that needs to be shown.
  2. The parameters property will be used to send custom parameters to this view controller that can be read upon initialisation. We will understand the importance of this variable later when transitioning to/from secondary segues.
  3. the delegate, named ContainerViewControllerDelegate, will allow the contained content view controller to communicate back to the container controller.

All of our contained view controller will subclass this subclass:

Now, when segueing from our container view controller, we will check if the destination segue is a CustomSplitContentViewController and call its setPerson:delegate:andParameters method to initialise all data for that controller in the prepareForSegue method:

ContainerViewControllerDelegate

This delegate will have just two methods:

  1. personModified will be called whenever a contained content view controller modifies the person that’s being shared across all the main-container-contained view controller hierarchy. We will  transmit this change back to the main view controller thanks to the homonymous method from the ContainerToMenuDelegate protocol at ContainerViewController, and also modify it at our ModelManager.
  2. segueRequestedWithIdentifier:parameters will allow a contained content view controller to ask for a transition to a different contained content view controller. This will be used, for instance, to go from the personal information of the user screen to editing one of the fields listed there.

This is how it will be implemented in ContainerViewController:

Notice how in segueRequestedWithIdentifier:parameters, we set the current parameters variable and call the segue. In prepareForSegue, those parameters will be sent to the destination contained content view controller, alongside the person and the delegate reference. The segue called in performSegue:sender will follow the 3rd path we specified in the Our Container View Controller explanation of prepareForSegue.

Our Contained Content View Controllers

We finally get to the leaves of our view controller hierarchy, the contained content view controllers. They will be subclasses of CustomSplitContentViewController, thus, when initialised, they will already have their context and data (person variable, parameters and the delegate) set. They will be able to use the person variable or call the delegate anytime. Apart from that, they will behave like any other UIViewController, as we have already set all the mechanisms in place to transition between them using the options in our Main view controller.

Secondary segues and transitions between contained VC

Let’s add a new functionality to our custom split view controller to illustrate the secondary segues between contained view controllers. Let’s add the ability to edit the information of a user by clicking on a field in the fields table view of the PersonalInformationViewController. First, we will add a new view controller to the IB, and create a subclass of CustomSplitContentViewController called ChangeFieldViewController. This view controller will be the destination of a segue from our ContainerViewController like the rest of contained content view controllers:

Then we will make sure to transition to that view controller from our PersonalInformationViewController in a way that ChangeFieldViewController receives the field that we want to modify from that person, alongside the current value:

As you can see, the PersonalInformationViewController has a table view with a set of fields (i.e: name, birth date, email, etc, modelled by the enumeration ModelFieldType). Each Person instance has a method valueForField: to retrieve the current value of that field in the person instance, and stringValueForField: to retrieve the string version of that value. In order to react to the user clicking on a field to edit its value, we will use the method tableView:didSelectRowAt: on the PersonalInformationViewController:

As you can see, the method will get the field and the string value for that field in our person instance. Then, we will build a parameters dictionary with those values and call our delegate’s method segueRequestedWithIdentifier:parameters: to trigger the segue with our parameters.

Now in ChangeFieldViewController we just need to receive this value in view will appear. As we are subclassing CustomSplitContentViewController, the parameters are already there for us to collect:

As an extra polishing detail, we can use two notifications to disable the menu when we are in a “secondary segue” and enable it again when we are in one of the main options view controllers. We can easily do that by listening in MainViewController to two notifications: kDLNotificationDisablePersonMenu and kDLNotificationEnablePersonMenu. Then, we will just call the first one in viewWillAppear on ChangeFieldViewController and the second one on the viewWillDisappear of that same view controller.

In MainViewController:

And in ChangeFieldViewController:

Where to go from here

That’s all! In this post, we learned how to implement a custom split view controller that will allow us to display different information in different contexts, formats and structures for every detail view controller. We can also customise everything from the visual appearance to the animations and transitions between our detail controllers. We also learned how to transmit information back and forth between our view controller hierarchy and set a completely functional replacement for UISplitViewController that can juice up your app.

In the next post of this series, we will tweak the interface a little more to make it work properly in iPhones where the detail view controller must behave like a full screen transition, but for now you can go to the github repository to download the whole source code and use it in your own projects. Do you have any suggestions? improvements? questions? let us know in the comments!

 

2 Comments
  • Jim Jones

    May 19, 2017 at 2:10 am Reply

    Any ETA on the next post? (iPhone support)

    • Ignacio Nieto Carvajal

      May 19, 2017 at 7:46 am Reply

      Hi there Jim!

      Thanks for your interest.
      The iPhone version is definitely in the queue, but I want to address some of the missing tutorials for the Start Page. Unfortunately, that probably means the Custom Split Controller will have to wait for a month at least.

Post a Comment

Before you continue...

Hi there! I created the Digital Tips List, the newsletter for Swift and iOS developers, to share my knowledge with you.


It features exclusive weekly tutorials on Swift and iOS development, Swift code snippets and the Digital Tip of the week.


Besides, If you join the list you'll receive the eBook: "Your First iOS App", that will teach you how to build your first iOS App in less than an hour with no prior Swift or iOS knowledge.

I hate spam, and I promise I'll keep your email address safe.