SwiftUI Views are written in a declarative style, which can be a nice, compact way to construct a large tree of sub-views, but it also can make it surprisingly hard to do certain things.

For example, suppose you’re debugging your View and you’re wondering about the value of some state variable at a certain time. You might try writing this:

struct MyView: View {
    var label: String
    
    // WARNING: this doesn't compile!
    var body: some View {
        print(“The label is: \(label)”)
        Text(label)
    } 
}

If you do, you’ll see this very un-helpful error:

Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type

Note: error messages are supposed to be much better in Swift 5.2, which is available in Xcode 11.4 as of yesterday, so things might already be better on that front, but the code still won’t compile.

Fix #1: add return

If you stare at that error message for a while, you might be inspired to try changing that last line to return Text(label), and in fact that will work.

Now suppose your View is a little more interesting, with more than one sub-view, and there are different things you want to log:

    // WARNING: this doesn't compile!
    var body: some View {
        VStack {
            print("The label is: \(label)")
            Text(label)
            print("There are \(label.count) characters")
            Text("abc")
        }
    }

This doesn’t work, and you can’t fix it by adding return. If you play around with it a little bit, like I did, you’ll find that you don’t get far by trying to treat these View expressions as regular code blocks. Instead, think of it as a special language just for Views, which just happens to use some of Swift’s keywords (like if and else).

Fix #2: put it in a regular function

You can get back to the familar world of “normal” Swift code by just putting the logging in a func and making it so the func’s result is part of the View expression. Here’s one way to do that:

    func trace<T>(msg: String, _ result: T) -> T {
        print(msg)
        return result
    }

Since this function can accept any value, you can wrap it around whatever part of the expression you like, which is either clever or ugly, depending on how you look at it:

    var body: some View {
        VStack {
            trace(msg: "The label is: \(label)",
                Text(label))
            Text(
                trace(msg: "There are \(label.count) characters", "abc"))
        }
    }

Fix #3: something prettier

A nicer idea (thanks to Rok Krulec here) is to set up your func to build a special View that doesn’t draw anything, but just does your logging when it’s constructed and then hopefully stays out of the way:

    func Print(_ msg: String) -> some View {
        print(msg)
        return EmptyView()
    }

    var body: some View {
        VStack {
            Print("The label is: \(label)")
            Text(label)
            Print("There are \(label.count) characters")
            Text("abc")
        }
    }

Pretty slick.

Fix #4: use the UI, Luke

On the other hand, if you’re using print to debug your UI, in a sense you’re doing it wrong — after all, you’re not writing a command-line app! What’s the UI equivalent of print? instead of spewing debug all over the console, how about spewing it all over the UI? Sounds great, right?

More practically, printing to the console doesn’t work when you’re using SwiftUI Preview to see the effect of your edits in real time. When you do that, print-ed output doesn’t seem to go anywhere as far as I can tell.

Here’s one way to make that happen:

    var body: some View {
        VStack {
            Text(label)
                .annotate("The label is: \(label)")
            Text("abc")
                .annotate("There are \(label.count) characters")
        }
    }
    
...

extension View {
    func annotate(_ msg: String) -> some View {
        self.overlay(
            Text(msg)
                .fixedSize(horizontal: true, vertical: true)
                .offset(x: 100, y: 10)
                .font(.caption)
                .foregroundColor(.gray)
        )
    }
}

This way, each line of debug output is displayed in the View itself. Each message floats below and to the right of the sub-view it’s attached to. Using overlay ensures that the View’s layout is unaffected by the dangling messages, and fixedSize allows each one to expand beyond the size of its corresponding View. The rest of the attributes just hopefully make the annotations a little less prominent so the UI is still recognizable.

Anyway, those are some things I’ve found to be interesting and perhaps useful. SwiftUI’s new, declarative style requires some new tools and some new ways of thinking. I’m enjoying experimenting to figure out what works, and I hope you found these experiments interesting, too.