Flow Controllers on iOS for a Better Navigation Control

Since I’m in London conversations with iOS developers have reached high levels with no doubts. I love to discuss with friends and iOS devs about new ways to improve our coding. Often my best practices are very appreciated among them and a bunch of devs start applying day-by-day what they learnt. “An Aspect Oriented Programming Approach to iOS Analytics” and “CocoaPods: Working With Internal Pods Without Hassle” are 2 examples of good best practices. A friend asked for a post about the specific topic of flow controllers so… here we go. :)

Navigation on iOS

There are very few ways to present UIViewControllers on iOS either through UINavigationController or UIViewController:

1
2
3
4
5
6
7
8
// UIViewController
[viewControllerInstance presentViewController:modalViewController
                                     animated:YES
                                   completion:^{ /* ... */ }];

// UINavigationController
[navigationControllerInstance pushViewController:detailViewController
                                        animated:YES];

The thing I never liked is that UIViewController instances have the ability to push things on their own using the associated UINavigationController and to present other UIViewController instances within their logic. It’s not… their responsibility.

The roots back to 2008

The design of the above APIs represents the easiest way to achieve the presentation of a detail view from a master one and I’m not surprised Apple approved it. With the launch of the iPhone in 2008, developers were given easy APIs to learn in order to ease the development learning curve: with just one line of code it was (and it is) possible to present other views.

So… we got used to the Apple APIs, maybe too much. Some of the APIs are not so great and it’s probably due to the old ones often ported from the Mac or from AppKit.

You are free to argue about it, but the UITableViewDelegate and UITableViewDataSource are another example: they mix presentation, callbacks and real datasource. Just to say that not everything that comes from Apple is perfect: sometimes it is debatable.

The need for a better world

What I want to introduce here is a more elegant and clean way to handle the presentation of UIViewControllers fulfilling these point:

  • UIViewControllers shouldn’t present other UIViewControllers
  • There should be a specific component responsible for handling the presentation flow

Good architectures like VIPER have been proposed and they try to solve more general problems in a clean way. What I explain here are small concepts to improve the way things are presented on screen. Be aware not to confuse with routing systems like JLRoutes whose main aim is to mimic the routing of web apps: we are not talking about them here.

Some new friends: the Flow Controllers

Over the years I spent time experimenting with different solutions to improve the code that handles the navigation of iOS Apps. Sometimes it involved a few components: Presenters, Factories, Flow Controllers… and in the end they all turned out to be over engineered and too much complicated solutions for a task that should be straightforward.

Here is the minimum way to achieve a separation of concerns between UIViewController logic and the app navigation:

  • Subparts of an app that require proper navigation control should be handled by specific objects called Flow Controllers
  • Flow Controllers control the transitions from different state/screens of the app
  • Flow Controllers inherits from NSObject and are logic classes
  • Flow Controllers are initialized with a UINavigationController instance
  • Flow Controllers are domain specific
  • Flow Controllers are passed around to the view controller they handle
  • Flow Controllers are responsible for creating other view controllers

Let’s talk code

It’s September 2014 and no, no Swift, I still write only in Objective-C, please bear with me. Consider a user profile view controller that usually links to following/followers/settings view controllers: it makes a lot of sense to have a flow controller to control the flow of them all.

Let’s call the ProfileViewController the ‘master’ and the following/followers/settings view controllers the ‘details’.

1
2
3
4
5
6
7
8
9
@interface ADBProfileFlowController : NSObject

- (instancetype)initWithNavigationController:(UINavigationController *)navigationController;

- (void)showFollowingsScreen;
- (void)showFollowersScreen;
- (void)showSettingsScreen;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@interface ADBProfileFlowController () <
  ADBFollowingsViewControllerDelegate,
  ADBFollowersViewControllerDelegate,
  ADBSettingsViewControllerDelegate
>

@property (nonatomic, weak) UINavigationController *navigationController;

@end

@implementation ADBProfileFlowController

- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {
    /* ... */
}

- (void)showFollowingsScreen {
    ADBDFollowingViewController *followingiewController = [[ADBDFollowingViewController alloc] initWithDependencies:...];
  followingViewController.delegate = self;
  
    if ([self _profileIsTopViewController]) {
        [self.navigationController pushViewController:followingViewController
                                             animated:YES];
    }
}

- (void)showFollowersScreen {
    ...
}

- (void)showSettingsScreen {
    ADBSettingsViewController *settingsViewController = [[ADBSettingsViewController alloc] initWithDependencies:...];
    settingsViewController.delegate = self;

    if ([self _profileIsTopViewController]) {
        [self.navigationController presentViewController:settingsViewController
                                                animated:YES
                                              completion:nil];
    }
}

#pragma mark - Private Methods

- (void)_profileIsTopViewController {
    return ([self.navigationController.topViewController isKindOfClass:[ADBProfileViewController class]]);
}

#pragma mark - ADBFollowingViewControllerDelegate

- (void)followingViewControllerDidReceiveTapOnCloseButton:(ADBFollowingViewController *)followingViewController {
    [self.navigationController popViewControllerAnimated:YES];
}

#pragma mark - ADBFollowersViewControllerDelegate

- (void)followersViewControllerDidReceiveTapOnCloseButton:(ADBFollowersViewController *)followersViewController {
    [self.navigationController popViewControllerAnimated:YES];
}

#pragma mark - ADBSettingsViewControllerDelegate

- (void)settingsViewControllerDidReceiveTapOnCloseButton:(ADBSetingsViewController *)settingsViewController {
    [settingsViewController savePreferences];
    [self.navigationController popViewControllerAnimated:YES];
}

The public interface is very domain specific. The profile view controller is

1
2
3
4
5
@interface ADBProfileViewController : NSObject

- (instancetype)initWithFlowController:(ADBProfileFlowController *)flowController;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@interface ADBProfileViewController ()

@property (nonatomic, strong) ADBProfileFlowController *flowController;

@end

@implementation ADBProfileViewController

- (instancetype)initWithFlowController:(ADBProfileFlowController *)flowController {
    /* ... */
}

- (IBAction)settingsButtonTapped:(id)sender {
    [self.flowController showSettingsScreen];
}

- (IBAction)followingsButtonTapped:(id)sender {
    [self.flowController showFollowingsScreen];
}

- (IBAction)followersButtonTapped:(id)sender {
    [self.flowController showFollowersScreen];
}

@end

To notice that the flow controller must not retain the navigation controller. Since the view controller retains the flow controller and the navigation controller retains the view controller, strongly referencing the navigation controller within the flow controller would cause a retain cycle.

Conclusions

I think this architecture has some advantages.

In general, might be that the detail needs other dependencies to be created: an ID or A DTO object won’t suffice and factories, providers, downloaders etc. might be necessary. We clearly don’t want to make the master being aware of all the crap needed to instantiate the details. The fan-out (the number of imports) in the master would increase with no benefit. It’d be good to let the flow controller be responsible for the creation of the view controllers of a specific subpart of the app.

The point here is to have domain specific flow controllers and the API that we want should reflect the intended behaviour. At this point, it’s not important if the flow controller pushes, presents modally or play chess against Big Blue to do its job (to show the desired screen).

Moreover, flow controllers can be reused on their own! They have all the necessary info to construct a specific sub-tree of navigation.

Another advantage is… decoupling! It’s always a good thing for ease of debugging in future.

One can get confused and still think that a view controller can be created in the view controllers and passed to the flow controller for handling just the presentation. Nope. If we pass the detail from the master to the flow controller just to present it, then it would be ok to present it directly from the master. The flow controller would be just a navigation controller wrapper with API like

  • (void)presentViewController:(UIViewController *)vc;
  • (void)pushViewController:(UIViewController *)vc;

Which is pointless and would create useless amount of code. This is totally against our goal: we aim for domain specific objects whose the goal to control the creation of specific objects and the presentation of them as well.

Thanks to Matteo Gobbi and Andrea Giavatto for the review of the article.


Comments