SwiftUI: Modified Content, Type Erasure, and Type Debugging

When working with declarative views, you should be able to reach for a full tool suite of functional application. In a typesafe language like Swift, things can prove more difficult than you’d might first think. Consider the following code:

What is core‘s type? It isn’t Text. It’s actually an application of modified content, specifically Text passed through a rotation effect:

Just add a background color and a shadow and the type jumps to this:

You might ask: why is this a problem? After all, Swift is doing all the heavy lifting, right? In my case, the answer lies in my struggle to incorporate this core image into a multi-stage bit of text art using reduce. Paul Hudson tweeted a step-by-step approach to this and I was sure I could make it simpler and more elegant.

And that’s where I started throwing myself against what at first seemed like an impenetrable wall for a couple of hours. Between SwiftUI’s stroke-style Dysarthria error messages and the typesafe system, my attempt at creating a solution along these lines felt doomed:

[Color.red, .orange, .yellow, .green, .blue, .purple].reduce(core) { view, color in
  view.padding()
    .background(color)
    .rotationEffect(theta)
}

The code wouldn’t compile and the error messages couldn’t tell me why. The problem? Each stage created a new layer of modified content, changing the type and rendering reduce  unable to do the work. It was only with the help of some deep-dives into the docs and advice from colleagues that I was able to arrive at a solution.

Type erasure, using SwiftUI’s AnyView struct enables you to change the type of a given view, destroying the nested hierarchy. Importantly, it creates a single type, allowing a reduce operation to proceed.

At first, I used AnyView the way you’d typecast in Swift, namely:

AnyView(view.padding()
  .background(color)
  .rotationEffect(theta))

But I hated that. It sticks out as so Un-SwiftUI-y, with the parentheses spanning multiple lines and throwing off the clear logical flow. If you’re going to go fluent, just go fluent. So, eventually, I decided to create a View type extension to handle this:

extension View {
  /// Returns a type-erased version of the view.
  public var typeErased: AnyView { AnyView(self) }
}

The result looks, instead, like this:

view.padding()
  .background(color)
  .rotationEffect(Angle(radians: .pi / 6))
  .typeErased

And yes, I went with a property and not a function as I felt this was expressing a core characteristic inherent to each View. I can probably argue it the other way as well.

From there, it wasn’t much of a leap to ask “what other fluent interface tricks can I apply”, and I ended up putting together this little View extensions for inline peeks:

extension View {
    /// Passes-through the view with debugging output
  public func passthrough() -> Self {
    print("\(Self.self): \(self)")
    return self
  }
}

This prints out an instance’s type and a rendering of the instance, which will vary depending on whether there’s a custom representation, passing the actual instance through to whatever the next stage of chaining is. I don’t use it much but when I do, it’s been pretty handy at taking a peek where Xcode’s normal QuickLook features hit the edge.

In any case, I thought I’d share these in case they’re of use to anyone else. Drop me a note or a tweet or a comment if they help. Cheers!

Update: It suddenly occurred to me that I could make this a lot more general:

extension View {
  /// Passes-through the view with customizable side effects
  public func passthrough(applying closure: (_ instance: Self) -> ()) -> Self {
    closure(self)
    return self
  }
}

Isn’t that nicer? The equivalent is now:

struct MyView: View {
  var body: some View {
    [Color.red, .orange, .yellow, .green, .blue, .purple]
      .reduce(Text("👭")
        .font(.largeTitle)
        .rotationEffect(Angle(radians: .pi))
        .typeErased)
      { view, color in
        view.padding()
          .background(color)
          .rotationEffect(Angle(radians: .pi / 6))
          .passthrough { print("\(type(of: $0)), \($0)") }
          .typeErased
    }
  }
}

And I can put any behavior in from printouts to timing to any other side effect I desire. To all the functional purists out there, I sincerely apologize. 🙂

One Comment

  • Thank you, Erica. This helped me pass a view to a function without having to worry about the complex type.