The Slow Shift to Swift: Refactoring Ancient Code

Jul 8, 2015

The oldest, least retouched, bit of code for Time Journal is in the AppDelegate, which is what iOS uses to allow developers to effect the launching and shutting down of their applications. Unfortunately, this was one of the first things I wrote for Time Journal. Probably written pretty quickly. And, without really considering the refactoring piece of things.

So, sticking with the premise of doing things different this time around, it pained me to convert the AppDelegate.

Here is the general concept of what the - (BOOL)application:willFinishLaunchingWithOptions: method looked like:

self.window.tintColor = [UIColor appTintColor];

self.tabController = (UITabBarController *)[[[[UIApplication sharedApplication] delegate] window] rootViewController];
self.tabController.delegate = self;
[(self.tabController.viewControllers)[0] setTitle:NSLocalizedString(@“track”, @“track”)];
[(self.tabController.viewControllers)[1] setTitle:NSLocalizedString(@“analyze”, @“analyze”)];
[(self.tabController.viewControllers)[2] setTitle:NSLocalizedString(@“report”, @“report”)];

UITabBar *tabBar = self.tabController.tabBar;
NSArray *tabBarItems = tabBar.items;
UITabBarItem *trackItem = tabBarItems[0];
UIImage *trackImage = [UIImage imageNamed:@“TrackTabSelected”];
trackImage = [trackImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
trackItem.selectedImage = trackImage;

UITabBarItem *analyzeItem = tabBarItems[1];
UIImage *analyzeImage = [UIImage imageNamed:@“AnalyzeSelected”];
analyzeImage = [analyzeImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
analyzeItem.selectedImage = analyzeImage;

UITabBarItem *reportItem = tabBarItems[2];
UIImage *reportImage = [UIImage imageNamed:@“ReportTabSelected”];
reportImage = [reportImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
reportItem.selectedImage = reportImage;

[self handleAppSettings];

return YES;

Yep, pretty horrible. It’s difficult to read. I’m a pretty big proponent of the DRY1 maxim when it comes to writing code, and this throws DRY out the window. We’re jumping from focusing on the AppDelegate variables, the window, then the tab bar…moving on!

The short version of what’s happening there:

  1. Set the window’s tint color to establish an overall color for the app, which all subviews will use; therefore, if it changes here, it changes everywhere.
  2. Get the UITabBarController.
  3. Set the delegate of the UITabBarController to be the AppDelegate.
  4. Set the title for each tab within the UITabBarController.
  5. The next three chunks set the selected image for each tab (the unselected image being handled by IB).
  6. Jump over to check the app settings bundle and change things accordingly.
  7. Do the important part; return that, yes, the application will, indeed, finish launching.

It is not difficult to refactor this to the exact same thing only using Swift syntax. But, nothing is gained. Note: The same architecture could be done in Objective-C:

if let w = window {
    w.tintColor = UIColor.tjTintColor()

    tabController = w.rootViewController as? UITabBarController
    tabController?.delegate = self
    if let tabBar = tabController?.tabBar {
        if let tabBarItems = tabBar.items as? [UITabBarItem] {
            let imageNames = [“TrackTabSelected”, “AnalyzeSelected”, “ReportTabSelected”]
            for i in 0..<tabBarItems.count {
                let selectedImage = UIImage(named: imageNames[i])?.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate)
                tabBarItems[i].selectedImage = selectedImage

            }
        }
    }

    if let VCs = tabController?.viewControllers as? [UIViewController] {
        let vcTitles = [“track”, “analyze”, “report”]
        for i in 0..<VCs.count {
            VCs[i].title = NSLocalizedString(vcTitles[i], tableName: “Localizable”, bundle: NSBundle.mainBundle(), value: vcTitles[i], comment: vcTitles[i])
        }
    }
    handleAppSettings()
    return true
}
return false

We’re getting there.

  1. Make sure there’s a window; otherwise, there’s no point in processing the rest of this code.
  2. If we don’t have a window, return false, because something has gone wrong.
  3. Otherwise, we continue:
    1. Set the tint color.
    2. Set a class-level tabBarController variable within the AppDelegate. Note: I’m not actually sure this is ever called from outside of the AppDelegate; so, I made a note to revisit.
    3. Grab the tabBar if it’s there.
    4. Grab the tabBarItems if they’re there.
    5. Set an array for our image names.
    6. Loop over the tabBarItems, and set the selected image for each tab during each enumeration.
    7. Grab the viewControllers if they’re there.
    8. Set an array of view controller titles.
    9. Loop over the viewControllers, and set the title for each view controller during each enumeration.
    10. Handle the application settings bundle.
    11. Return true if we made it this far.

A couple of things though:

  1. We should always have the same number of viewControllers as we do tabBarItems; therefore, it’s possible to combine these into one loop—loops being a pretty expensive computational task.
  2. All of the indents to verify things makes it pretty difficult to read; especially since we are essentially setting up the tab bar—not affecting the window or the AppDelegate.

So, with that in mind.

// willFinishLaunching
if let w = window {
    w.tintColor = UIColor.tjTintColor()
    tabController = w.rootViewController as? UITabBarController
    tabController?.delegate = self
    if let tController = tabController {
        setupTabBar(tController)
    }
    handleAppSettings()
    return true
}
return false

// setupTabBar(tabController: UITabBarController)
let tabBarItems = tabController.tabBar.items as? [UITabBarItem]
let VCs = tabController.viewControllers as? [UIViewController]
if tabBarItems?.count == VCs?.count {

    let imageNames = [“TrackTabSelected”, “AnalyzeSelected”, “ReportTabSelected”]
    let vcTitles = [“track”, “analyze”, “report”]

    for i in 0..<tabBarItems!.count {

        let tabBarItem = tabBarItems![i] as UITabBarItem            
        let selectedImage = UIImage(named: imageNames[i])?.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate)
        let vc = VCs![i] as UIViewController

        tabBarItem.selectedImage = selectedImage
        vc.title = NSLocalizedString(vcTitles[i], tableName: “Localizable”, bundle: NSBundle.mainBundle(), value: vcTitles[i], comment: vcTitles[i])
    }
}

Much better.

  1. We still do everything up until setting up the tabBar.
  2. Call a method for setting up the tabBar.
    1. Get items and view controllers as optionals from the non-optional tabController; casting both as Swift Arrays of their value type to improve safety and get more help from the compiler.
    2. Verify counts are the same.
      • Set the image names and view controller titles.
      • Iterate over the tabBarItems, setting the selected image and title with each enumeration through the loop. Note: If tabBarItems was nil, we never would have made it this far; therefore, it is safe to unwrap the optional in the loop opening without fear of crashing..

Now, in the future, if we decide to add another tab to the app, all we need to do is update the two arrays with a title and the image name. We could also rearrange the images and tabs (though not their destinations) by rearranging those two arrays. Further, if we find out the tabController variable in the AppDelegate isn’t necessary, deleting it only affects the willFinishLaunching method. Finally, when we look at the willFinishLaunching method now, it is easy to see what we are doing there—setting up the window and AppDelegate as a whole; only needing to go elsewhere to find (and execute) details (subcomponents of the window)—kind of like a footnote.2


  1. When you repeat the same code, changing only one or two things, it leads to a configuration management nightmare when you try to make changes later. return to text

  2. Nah, I’m just a footnote. return to text

###