Is it possible to "decorate" the last element of a parameter pack?

I am trying to write a SwiftUI view that adds a special modifier to the final view inside it. Because I am using SwiftUI, the "shape" of the data coming in is controlled by what is acceptable to return from a @ViewBuilder closure; I specifically don't want to use this to enforce something like "you must always add a particular modifier to the last view or your code won't type check," I want to do this for the consumers of the API internally.

I thought I should be able to do it by writing the following:

func topLevelFunction<each T, U>(t: () -> (repeat each T, U)) {
    let result = t()
    tupleViewInit(t: (result.0, Decorate(decorated: result.1)))
}

func tupleViewInit<T>(t: T) {
    
}
                  
struct Decorate<T> {
    var decorated: T
}

Unfortunately this fails, the compiler complains that "Value pack expansion can only appear inside a function argument list or tuple element."

Interestingly, this compiles:

func topLevelFunction<each T, U>(t: () -> (repeat each T, U)) {
    let result = t()
    tupleViewInit(t: result)
}

It feels like parameter packs are "look, don't touch" today, in the sense that I can pass this object around as much as I want, or turn it back into a tuple, but I can't do anything with any of the stuff inside it without losing the information that the last element was of type U.

My other thought was to try to write some function that can take in a whole parameter pack, and somehow understand that I'm at the end, but I can't think of any way to do that.

My final, nuclear option, is to manually grab the memory out of the parameter pack and the last element of the tuple, then rearrange it into something that is what I want, then force cast it back into (repeat each T, Decorated<U>), but this seems like a bad idea on many levels.

Any advice?

Idk how to do this with parameter packs, but in SwiftUI there is private API (that will most likely never change) that would allow you to do this. A good article going through it: SwiftUI under the Hood: Variadic Views - Moving Parts

1 Like

There is a pack iteration proposal: swift-evolution/proposals/0408-pack-iteration.md at main · apple/swift-evolution · GitHub

I suggest the following toy variant:

func foo<each T>(_ value: () -> (repeat each T)) where T: View {
  var lastView: T?
  for value in repeat each value {
    let result = value()
    ... do smth.
    lastView = result
  }

  if let lastView {
    let decoratedLastView = Decorated(view: lastView)
  }
}

The example above can be modified with addition of totalCount, providing a way to access first, second, last, pre-last ... values.

I should clarify that I am not currently using Swift 6, so for loops on parameter packs are not available to me.

This appears to exactly address the problem I currently have: knowing whether to add a divider or not. As you observed, it doesn't address what I asked, but I'll take it. Thanks!

Ok, here is rewritten example for Swift 5.9:

func foo<each T>(_ builder: () -> (repeat each T)) where repeat each T: View {
  func doTheStuff<V: View>(forView view: V, _ ext: inout (any View)?) {
//    print("doTheStuff \(V.self)")
    ext = view
  }
  
  let views = builder()
  
  var last: (any View)?
  repeat doTheStuff(forView: each views, &last)
  
  if let last {
    print("last view is type of: \(type(of: last))")
  }
}

foo {
  (Text(""), Button("", action: {}), VStack { Text("").padding() })
}
// prints: last view is type of: VStack<ModifiedContent<Text, _PaddingLayout>>

The func implementation can be updated later with migration to Swift 6 with no need to change its interface.

I guess this should work with what's implemented in Swift 5.9

@resultBuilder struct Splitting {
    static func buildPartialBlock<A>(first content: A) -> ((), A) {
        return ((), content)
    }

    static func buildPartialBlock<each A, B, C>(
        accumulated result: ((repeat each A), B), next: C
    ) -> ((repeat each A, B), C) {
        return ((repeat each result.0, result.1), next)
    }
}

struct MyView<each V, Last> {
    let content: (repeat each V, Decorate<Last>)

    init(
        @Splitting _ content: () -> ((repeat each V), Last)
    ) {
        let views = content()
        self.content = (repeat each views.0, Decorate(decorated: views.1))
    }
}
1 Like