Articles, podcasts and news about Swift development, by John Sundell.

Building SwiftUI debugging utilities

Published on 23 Aug 2020
Discover page available: SwiftUI

When working on any kind of app or system, it’s almost guaranteed that we’ll need to debug our code at one point or another. For example in order to reproduce a bug that’s been reported to us by either a tester or a user, to track down a race condition or some other source of ambiguity within our logic, or to fix a view that’s not being rendered correctly.

Although Xcode ships with a quite comprehensive suite of debugging tools — such as the LLDB debugger, the Instruments app, and various view and layout inspectors — sometimes building our own, custom set of debugging utilities can be incredibly useful, especially when it comes to UI development.

This week, let’s take a look at a few examples of doing just that when it comes to SwiftUI-based views in particular.

Printing values

Using print to output a given value within the Xcode console might sometimes simply be seen as a more crude and less powerful alternative to using breakpoints and interacting with the debugger directly. However, while printing might be a somewhat simple technique, it can still be incredibly useful — especially within situations when we don’t wish to stop our program’s execution while debugging its state.

As an example, let’s say that we’re working on the following EventView, which uses a view model to determine what data to render:

struct EventView: View {
    @ObservedObject var viewModel: EventViewModel

    var body: some View {
        VStack {
            Image(uiImage: viewModel.bannerImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
            Text(viewModel.title).font(.title)
            Text(viewModel.formattedDate)
            ...
        }
    }
}

Now let’s say that the above Image isn’t being rendered as we’d expect, and that we’re suspecting that the size of our view model’s bannerImage might be the source of the problem. To find out whether’s that’s actually the case, one possible approach would be to set a breakpoint within our body implementation, and to then run the command po viewModel.bannerImage within the debugger once our program was stopped at our breakpoint.

While that’s certainly a fine approach, there are situations in which breakpoint-based debugging isn’t very practical — for example if we’re using a Swift playground to develop a given view (which doesn’t give us direct access to the debugger), or if we’re expecting our data to be rapidly changed, in which case our breakpoint might be repeatedly triggered and our execution constantly stopped.

So let’s say that we instead would like to print the size of our view model’s bannerImage each time that our view is rendered, to easily be able to see how that value changes over time by looking at Xcode’s console. One way to do that would be to simply call print within our body implementation, and to then use an explicit return statement when creating our top-level VStack — like this:

struct EventView: View {
    @ObservedObject var viewModel: EventViewModel

    var body: some View {
        print(viewModel.bannerImage.size)

        return VStack {
            ...
        }
    }
}

While the above approach does work in this particular case, it would arguably be nicer if we would be able to issue print commands without having to modify how we create and return our view hierarchy — for example by enabling us to print values just like how we apply modifiers to our views.

To make that happen, we could write a very lightweight View extension method that simply forwards the call to print any value to Swift’s native print function, and then returns the view that it was called on:

extension View {
    func print(_ value: Any) -> Self {
        Swift.print(value)
        return self
    }
}

Above we’re calling the built-in print function using Swift.print, to let the compiler know that we’re not looking to recursively call our own custom print method.

With the above in place, we can now simply append our call to print at the end of our view declaration, just like how we’d apply a modifier to it, without requiring us to make any additional changes to the structure of our view’s body:

struct EventView: View {
    @ObservedObject var viewModel: EventViewModel

    var body: some View {
        VStack {
            ...
        }
        .print(viewModel.bannerImage.size)
    }
}

Really nice! However, having our new custom method use the exact same name as the system’s own print function does have a few downsides in this case — since any call to print within one of our view body implementations will now be routed to our custom method by default — which in turn requires us to always type out Swift.print whenever we want to use the system function, which could quickly become quite inconvenient.

Performing custom debugging actions

While we could simply fix that problem by renaming our print method to something else, let’s also take this opportunity to improve things even further. Since print is likely not the only debugging action that we’d like to perform while working on our various views — let’s create a more generalized method that’ll let us run any sort of debugging closure within our views, and to only actually run such a closure when our app is running in DEBUG mode — like this:

extension View {
    func debugAction(_ closure: () -> Void) -> Self {
        #if DEBUG
        closure()
        #endif

        return self
    }
}

Another option would be to place the above #if DEBUG condition around our whole extension instead, to force ourselves to remove any calls to debugAction before building a release version of our app. To learn more about that kind of conditional compilation, check out last week’s “Using compiler directives in Swift”.

Using our new debugAction abstraction, we can now replace our previous printing modifier with a new one called debugPrint, which (thanks to the above conditional compilation) only actually prints a value within debug builds:

extension View {
    func debugPrint(_ value: Any) -> Self {
        debugAction { print(value) }
    }
}

struct EventView: View {
    @ObservedObject var viewModel: EventViewModel

    var body: some View {
        VStack {
            ...
        }
        .debugPrint(viewModel.bannerImage.size)
    }
}

By prefixing each of our debugging modifier methods with debug, we make it crystal clear that these APIs are only meant to be used for debugging purposes, and since we now have that generalized debugAction abstraction, we can easily run any custom closure that we’d like when debugging one of our views — for example in order to assert that a view’s data fulfills certain requirements:

struct EventView: View {
    @ObservedObject var viewModel: EventViewModel

    var body: some View {
        VStack {
            ...
        }
        .debugAction {
            assert(viewModel.bannerImage.size.width > 400)
        }
    }
}

Debug-specific modifications

While being able to perform custom actions already covers a lot of ground when it comes to debugging UI code, sometimes we might also want to modify our views in order to visualize some form of layout problem, or to be able to easily see what the actual frames of our views are.

To make that happen, let’s create a second general-purpose debugging utility called debugModifier, which will let us apply a closure-based modifier to any of our views, and we’ll once again only perform that operation within debug builds:

extension View {
    func debugModifier<T: View>(_ modifier: (Self) -> T) -> some View {
        #if DEBUG
        return modifier(self)
        #else
        return self
        #endif
    }
}

Using the above, we can now build any number of specialized debugging utilities that’ll let us visualize our views in various ways. For example, the following two utilities enable us to quickly apply either a debug border or background color to any of our views, to easily be able to see what sort of space they take up at runtime:

extension View {
    func debugBorder(_ color: Color = .red, width: CGFloat = 1) -> some View {
        debugModifier {
            $0.border(color, width: width)
        }
    }

    func debugBackground(_ color: Color = .red) -> some View {
        debugModifier {
            $0.background(color)
        }
    }
}

Here’s how we might use the above two utilities to visualize the frames of the various components that make up our EventView:

struct EventView: View {
    @ObservedObject var viewModel: EventViewModel

    var body: some View {
        VStack {
            Image(uiImage: viewModel.bannerImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
            Text(viewModel.title).font(.title)
                .debugBackground()
            Text(viewModel.formattedDate)
                .debugBackground(.green)
            ...
        }
        .debugBorder()
    }
}

Although applying a border or background color isn’t a huge task by any means, having the above kind of dedicated debugging utilities really makes it easy to do so, and also ensures that any such debugging code will never be evaluated when our app is deployed to production.

Conclusion

Although it might seem rather strange for someone to write an article about print and background color-based debugging in 2020, I’ve really found the above kind of techniques to be incredibly useful when building SwiftUI views, and hope that you will too.

Of course, neither of the above utilities are complete replacements for the LLDB debugger, breakpoints, profiling, and other kinds of more advanced debugging techniques — but they’re rather simple utilities that we can reach for whenever we want to quickly debug a piece of our UI.

Plus, having those general-purpose debugAction and debugModifier methods in place lets us build all sorts of custom SwiftUI debugging utilities in a uniform way, without ever having to risk that such code is executed in production builds.

How do you typically debug your SwiftUI views? Let me know, along with any questions, comments and feedback that you might have, either via Twitter or email.

Thanks for reading! 🚀