How to build a debug view for your iOS app 📱

6 minute read

This article is meant as an introduction to an iOS debugging library hosted on our company GitHub: https://github.com/worldline/ControllerFactory

iOS apps are complex pieces of software. Nowadays it is not unlikely to run into apps that aggregate more than a hundred view controllers. When you factor in the fact that some of them might be only accessible through specific business scenarios, you often end up being tangled in a situation where displaying a particular controller on screen is proven to be quite a hardship.

Such a situation can be a threat to the quality of an app, especially at times when developers are required to check every screen, for instance to ensure that they will work well on the iPhone X.

A pretty handy tool for that job would be a debug view from which any controller could be instantiated and presented on screen. So how can we build such a tool?

A good starting point

The first step is to figure out how to instantiate an arbitrary controller. This is going to be the easy part, because this feature is already implemented in Foundation via the function public func NSClassFromString(_ aClassName: String) -> Swift.AnyClass? which, along with a few more code, basically allows the developer to instantiate any object, using only the name of its class.

So now what we need to put together is a list of all the view controllers in our app. If we had no other options, we could store them manually in a .plist file, but this approach would have the downside of requiring a manual update every time is class is created, deleted or renamed, which is tedious and extremely error prone. Fortunately, there is a much better alternative.

Leveraging the Objective-C runtime

You might not be aware of this, but every iOS app runs over a lightweight runtime environment. Much can be said regarding this runtime, and if you want to learn about it in details, feel free to have a look at the documentation provided by Apple.

For the purpose of this article, we are going to limit our explanation to the fact that when an iOS app launches, all its Objective-C compatible classes are loaded in the runtime, and the runtime keeps references to them as long as the app is alive, which makes this runtime an excellent source from which we can retrieve a list of all the view controllers.

How can we implement this in practice? First we start by importing the API that interacts with the runtime:

import ObjectiveC

Then we write the logic to actually fetch the list of classes:

// 1
extension Bundle {

    var controllerNames: [ControllerName] {
        //2
        guard let bundlePath = self.executablePath else { return [] }
        
        // 3
        var size: UInt32 = 0
        var viewControllers = [ControllerName]()
        
        // 4
        let classes = objc_copyClassNamesForImage(bundlePath, &size)

        // 5
        for index in 0..<size {
            if let className = classes?[Int(index)],
                let name = NSString.init(utf8String:className) as String?,
                NSClassFromString(name) is UIViewController.Type
            {
            	// 6
                let split = name.components(separatedBy: ".")
                let displayValue = split.count > 1 ? split[1] : split[0]

                viewControllers.append(ControllerName(className: name, displayValue: displayValue))
            }
        }

        // 7
        return viewControllers.sorted(by: { $0.displayValue < $1.displayValue })
    }
}

Let us get into the details of this code:

  1. compiled classes are stored inside a Bundle, so we make that type our starting point, by creating an extension for it. This allows us to restrict our query to a particular module, and avoid retrieving classes from system or vendor frameworks.
  2. we make sure that the bundle is loaded in memory by taking its executablePath
  3. since we are going to interact with a C API, an array will be returned as both a pointer to the first element and a value for its size
  4. the function objc_copyClassNamesForImage returns the list of all the Objective-C compatible classes inside the bundle as an array of C-style strings (i.e. null-terminated char *)
  5. we then map those C-style strings to String values, map those to actual Type objects using NSClassFromString, and finally filter out non-UIViewController classes
  6. we compute a displayValue that removes potential namespaces
  7. finally, we sort the values by their displayValue, and return the result

Building a nice debug view

Now that we have built ourselves a nice tool, it is time keep on our goal and use it to build a nice debug view inside our app.

For simplicity, we are going to do this in the form a simple table view:


import UIKit
import ControllerFactory

class ControllerFactoryViewController: UIViewController {

    let viewControllerNames = Bundle.main.controllerNames
    
    @IBOutlet weak var tableView: UITableView!
}

We then add the necessary logic to populate the table view:

extension ControllerFactoryViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewControllerNames.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "FactoryViewControllerCell")!
        
        cell.textLabel?.text = viewControllerNames[indexPath.row].displayValue
        
        return cell
    }
}

And finally we instantiate the chosen view controller and push it on the navigation stack:

extension ControllerFactoryViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let newController = viewControllerNames[indexPath.row].init()
        
        show(newController, sender: self)
    }
}

That’s it, we now have a fully functioning debug view in our app 🎉

Debug in action

Dealing with more advanced use cases

What we have achieved is very nice, but in most cases it will be to simple to be really useful. When you think about it, we are able to instantiate a view controller, but we are unable to provide it with any kind of initial data.

In a real world project, this issue would most certainly prove to be a deal breaker, as many view controllers are likely to rely on some initial data passed on by a previous screen.

So how do we improve? First we need a way for a controller to provide both a list of its use cases and how to handle one of them. To do so we declare a protocol:

@objc public protocol ControllerFactoryUseCaseCompliant {
    static func getUseCases() -> [String]
    func prepareForControllerFactory(useCase: String)
}

Then, we modify our delegate method to distinguish between the view controllers that implement this protocol from those that don’t:

extension ControllerFactoryViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let newControllerType = viewControllerNames[indexPath.row]
        
        // 1
        if let newControllerType = newControllerType as? (UIViewController & ControllerFactoryUseCaseCompliant).Type {
            let alert = UIAlertController(title: "Choose a use case", message: nil, preferredStyle: .actionSheet)
            // 2
            let instantiation = { (useCase: String) in
                alert.dismiss(animated: true, completion: nil)
                let newViewController = newControllerType.instantiate.init()
                (newViewController as? ControllerFactoryUseCaseCompliant)?.prepareForControllerFactory(useCase: useCase)
                show(newViewController, sender: self)
            }
            
            // 3
            let actions = newControllerType.useCases.map {
                UIAlertAction(title: $0.capitalized, style: .default, handler: { (action) in
                    instantiation(action.title!.lowercased())
                })
            }
            
            actions.forEach(alert.addAction)
            
            present(alert, animated: true, completion: nil)
        } else {
        	// 4
            show(newControllerType.init(nibName: nil, bundle: nil), sender: self)
        }
    }
}
  1. we use the operator & introduced with Swift 4, that allows us to indicate that we want to try to cast to a type that is a UIViewController and implements UseCaseInstantiable
  2. for the sake of factorisation, we define a closure to instantiate the view controller from its initial data
  3. for each use case the controller handles, we add an action to the UIAlertController
  4. when the type does not implement ControllerFactoryUseCaseInstantiable we instantiate it like we did before

Once we run our app, we can see that we are indeed able to provide some initial data to our controller:

Debug view with use case

Conclusion

Reflexive programming is a powerful tool, and this particular instance of its use makes no exception: with a few line of code, we were able to put together a factory that is easy to include in an existing app, and allows its developer to instantiate any view controller from a nice debug view.

Such a tool will not doubt be a welcome addition when making sure that a legacy app runs smoothly on the iPhone X, but it could also prove to be handy anytime you want to perform non-regression test on a section of that app that is normally annoyingly complicated to reach.

If you want to use the code above in your project, feel free to do so, it is available on our company GitHub: https://github.com/worldline/ControllerFactory.


Written by

Vincent Pradeilles

Lead iOS software engineer

Benoît Caron

Lead iOS software engineer