Reading time: 3 min

Here's a quick tip for resolving a common and annoying error that you'll occasionally see. The error message is

Function declares an opaque return type, but the return statements in its body do not have matching underlying types

and you can see it if you declare a function or a property that returns some View and the body has multiple returns.

OK, let's break it down! Let's say you're trying to render a custom view that uses an HStack, but wish to take advantage of LazyHStack for improved performance. So, you do implement an property that returns opaque type some View:

private var hStack: some View {
  if #available(iOS 14.0, *) {
    return LazyHStack(alignment: .center, spacing: spacing) {
      itemRenderer
    }
  } else {
    return HStack(alignment: .center, spacing: spacing) {
      itemRenderer
    }
  }
}

However, XCode won't too happy with that solution:

Issue

Now you'll probably (hopefully :]) go and read up on opaque types, and learn that:

If a function with an opaque return type returns from multiple places, all of the possible return values must have the same type.

OK, so let's add another layer of opaqueness in order to fool the compiler:

private var hStack: some View {
  if #available(iOS 14.0, *) {
     return lazyHStack
  } else {
    return oldHStack
  }
}

private var oldHStack: some View {
  HStack(alignment: .center, spacing: spacing) {
    itemRenderer
  }
}

@available(iOS 14.0, *)
private var lazyHStack: some View {
  LazyHStack(alignment: .center, spacing: spacing) {
    itemRenderer
  }
}

However, it still won't work:

The error above might seem ridiculous, but it makes sense given the opaque type constraints listed above - the compiler is, apparently, smart enough to realize you're trying to play around it, and holds its ground.

Solution #1: add @ViewBuilder

The first thing you should try is to place the code in a function/computed property and mark it with @ViewBuilder:

@ViewBuilder private var hStack: some View {
  if #available(iOS 14.0, *) {
     LazyHStack(alignment: .center, spacing: spacing) {
       itemRenderer
     }
  } else {
    HStack(alignment: .center, spacing: spacing) {
      itemRenderer
    }
  }
}

You'll probably need to make a few more adjustments (like removing explicit returns), but this should work most of the time. It offers no performance penalty and allows you to keep writing your code in a way idiomatic to SwiftUI. The only potential downside is that you can't use all control flow statements as they aren't permitted, but again, you're already used to writing SwiftUI code that way.

Solution #2: wrap in AnyView

If @ViewBuilder approach doesn't work, try wrapping your views in AnyView. While the AnyView docs contain a scary proposition that:

Whenever the type of view used with an AnyView changes, the old hierarchy is destroyed and a new hierarchy is created for the new type.

profiling investigations haven't found decreased performance, especially on simple examples such as this one. So, let's apply that suggestion to our example:

private var hStack: some View {
  if #available(iOS 14.0, *) {
    return AnyView(LazyHStack(alignment: .center, spacing: spacing) {
      itemRenderer
    })
  } else {
    return AnyView(HStack(alignment: .center, spacing: spacing) {
      itemRenderer
    })
  }
}

Now everything should build and run properly! This example illustrates an all-too-common situation in SwiftUI, where your goal and the compiler are at odds with each other, despite both being right to an extent: the developer use-case of conditionally applying a view type is perfectly legitimate, but so is the compiler when enforcing the rules that govern opaque types. Luckily, AnyView comes to rescue when all the other options are exhausted.

Next Post Previous Post