Edition V

The Masilotti Monthly

Sign up for the ⚡️ Hotwire dev newsletter

A monthly newsletter on Turbo (Native), Stimulus, and Strada articles, code, courses, and more. Plus, exclusive insights into how I build Turbo Native apps.

By subscribing, you agree with Revue’s Terms of Service and Privacy Policy.

In this month’s newsletter, I’m revealing a behind-the-scenes look at how I test code that interacts with UIAlertController and UINavigationController. Also, I’m working on a secret project that I would love your input on!

How to test code that uses UINavigationController

Testing code that uses the navigation stack starts off simple but can quickly devolve into bad practices like ticking the run loop, adding unneeded expectations, or (worse) throwing in sleep statements.

Developers reach for these techniques because pushing a view controller onto a navigation stack isn’t instantaneous. There’s a small delay between calling pushViewController(_:,animated:) and when viewControllers is set.

let navigationController = UINavigationController()

// The first controller is always pushed without animation.
navigationController.push(UIViewController(), animated: true)
XCTAssertEqual(navigationController.viewControllers.count, 1)

// Fails! Subsequent controllers take a half-second or so to animate.
navigationController.push(UIViewController(), animated: true)
XCTAssertEqual(navigationController.viewControllers.count, 2)

To remedy this I add the following code snippet to almost every iOS project I work on. It’s a tiny subclass of UINavigationController that lets us “spy” on what methods were called.

class NavigationControllerSpy: UINavigationController {
    private(set) var pushedViewControllers = [UIViewController]()

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        super.pushViewController(viewController, animated: animated)

Create an instance of this spy and inject it into your subject under test. Instead of reaching for viewControllers, we use our new property: pushedViewControllers. This keeps track of everything that was pushed — synchronously and without delay. It also calls super, so the UINavigationController behavior is preserved.

This can also be extended to work with present(_:,animated:,completion:) following the same spying pattern!

How to test code that uses UIAlertController

Up until recently, I’ve recommended wrapping UIAlertController and testing that. But I’ve found a better solution, and one that can test existing code without any additional changes.

It’s pretty straightforward to test the title, message, and action names of UIAlertController. Everything is exposed publicly. But things get tricky when we want to assert what happens when we tap on a button.

UIAlertAction has no handler property. But lucky for us we can find it – if we know where to look. Add this snippet to your test suite and you can start asserting what happens when you tap an alert’s action – with no changes to your application code needed.

extension UIAlertController {
    typealias AlertHandler = @convention(block) (UIAlertAction) -> Void

    func tapButton(titled title: String) {
            let action = actions.first(where: { $0.title == title }),
            let block = action.value(forKey: "handler")
        else { return }

        let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)

Hybrid iOS apps with Turbo

I officially passed the halfway mark in my 6-part series on building hybrid iOS apps with Turbo. If you already have a mobile-friendly Ruby on Rails website, Turbo provides a great foundation to build a feature-complete iOS app in a fraction of the time of a fully native app. (Feel free to catch up on the Turbo framework and URL routing first.)

Part 3 in the series touches on forms and basic authentication. It covers how to share cookies between your web views and sign in via your existing web-based code.

Part 4 dives deep into the JavaScript bridge. This is used to pass messages between the client and server in between request cycles. There’s an example on how to add a native button to specific screens and dynamically change the design and behavior – all via your server’s code!

Stay tuned for part 5 and 6, which will touch on native screens (including native sign in) and architecture best practices with the coordinator pattern.

XCTest cheat sheet

I’m working on a secret project and would love your input.

I’m compiling a cheat sheet of XCTest techniques and best practices. It’s for beginners and experts alike and is going to include lots of little tips and tricks I’ve picked up over the years.

So far, it covers basic assertions, different approaches to waiting for expectations, how to test specific UIKit classes, and how to test networking. The code snippets at the beginning of this newsletter are great examples.

So, my faithful subscriber, what else would you like to see included? Is there some XCTest syntax you always forget and have to Google? Or somewhere that you always get caught up when testing your apps?

Let me know and I’ll make sure to include it in the first release! You can email me or reach out on Twitter.

Sign up for the ⚡️ Hotwire dev newsletter

A monthly newsletter on Turbo (Native), Stimulus, and Strada articles, code, courses, and more. Plus, exclusive insights into how I build Turbo Native apps.

By subscribing, you agree with Revue’s Terms of Service and Privacy Policy.