Search code examples
iosswiftfunction-builder

Can one use generic sub classes with function builders in Swift?


While using function builders, i'm unable to use the variadic return builder closure on an object in conjunction with subclassing + generics. The Swift Playground can be found here.

First, lets define our function builder. In this case, I will be using one which supports both array + variadic initialization, as well as generics:

@_functionBuilder struct Builder<T> {

    static func buildBlock(_ items: T...) -> [T] {
        items
    }

    static func buildBlock(_ items: [T]) -> [T] {
        items
    }
}

Next, I defined a generic container object which supports builder initialization, and retains an array of objects.

class Container<T> {

    let items: [T]

    init(@Builder<T> builder: () -> [T]) {
        self.items = builder()
    }
}

Finally, i've sub-classed this object to define a "Navigation Controller" abstraction:

  struct View { }

  final class NavigationController: Container<View> { }

  let nav = NavigationController {
      View()
  }

This is where the following error is produced by Xcode: Cannot convert value of type 'View' to closure result type '[View]'.

This seems odd because the function builder should know to use the variadic initializer instead of the array initializer, but no.

To test my theory, I defined an object which did not sub-class another, but used the builder syntax as it's defined in my generic Container implementation.

  class Nav {

      let views: [View]

      init(@Builder<View> builder: () -> [View]) {
          views = builder()
      }
  }

  let y = Nav {
      View()
  }

This work's fine, and compiles and runs without issue. It would seem that subclassing combined with generics confuses the compiler about which function builder initialization definition to use, the one that returns an array or the one that returns a variadic list.

Has anyone run into this before?


Solution

  • This issue isn't related to generics. What happens here is that the automatic initializer inheritance doesn't "inherit" the attribute @Builder<T>, so effectively, NavigationController initializer is of the following form:

    init(builder: () -> [View]) {...}
    

    You can override the init:

    final class NavigationController: Container<View> {
        override init(@Builder<View> builder: () -> [View]) {
            super.init(builder: builder)
        }
    }