Instantiate a parameter pack from an array

We have an app that creates controls to set parameters on a component view. We do this with parameter packs, its great and saved us a lot of time when adding new components.

e.g.

struct SandboxView<each Option, Content: View> {
  init(
    _ options: repeat each Option,
    @ViewBuilder content: @escaping (repeat each Option) -> Content
  ) {
    // create controls and the `content` view.
  }
}

We just need show a different SandboxView for each component we have.
(Inside SandboxView we add SwiftUI controls for each option, e.g. a Picker for enums, a TextField for strings.)

SandboxView(ButtonTheme.Corner.rounded,
            "Button Title"
) { corner, title in
  ButtonComponent(variant: variant, title: title)
}

Now we want to also show the components on watchOS, we are trying to send the parameters via WatchConnectivity so sending Codable encoded data to the watch.

We can send a custom struct for each component, and we can encode and decode our parameter pack!

struct DataWithType: Codable {
  let data: Data
  let type: SupportedType
}
struct WatchContainerSimple: Codable {
  var collection: [DataWithType] = []

  init<each Option: Codable>(options: repeat each Option) throws {
    for option in repeat each options {
      let supportedType: SupportedType
      switch type(of: option) {
      case is Bool.Type: supportedType = .bool
        // more types....
      default: throw CodingError.unsupportedType
      }
      let data = try JSONEncoder().encode(option)
      collection.append(DataWithType(data: data, type: supportedType))
    }
  }

  init(from decoder: any Decoder) throws {
    var collection: [DataWithType] = []
    let codableWithTypes = try decoder.singleValueContainer().decode([DataWithType].self)
    
    for codableWithType in codableWithTypes {
      collection.append(DataWithType(data: codableWithType.data, type: codableWithType.type))
    }
    self.collection = collection
  }
}

Is it possible to programmatically instantiate a parameter pack from an array or dictionary? It might be hacky but we can send values to indicate the type we want to use for each type in the pack, but we are not sure how to instantiate it.

In the WWDC video Generalize APIs with parameter packs the host says "The result is a comma-separated list of types..." and obviously arrays can be instantiated with comma separated values but can that array be passed in a way that works with a parameter pack?

Is there a way to create a parameter pack without using the repeat, each pattern?

Sorta kinda. I might be able to help with that, but I don't understand why you're not just sending tuples.

@Test func codable() throws {
  @Pack var container = (1, true, "three")
  @Pack(
    projectedValue: try JSONDecoder().decode(
      Pack<Int, Bool, String>.self,
      from: JSONEncoder().encode($container)
    )
  ) var decoded
  #expect(decoded == container)
}
/// A workaround for not being able to extend tuples or parameter packs directly.
///
/// - Note: Only single values or unlabeled tuples are supported.
@propertyWrapper public struct Pack<each Element> {
  public init(wrappedValue: (repeat each Element)) {
    self.wrappedValue = wrappedValue
  }

  public init(projectedValue: Self) {
    self = projectedValue
  }

  public var wrappedValue: (repeat each Element)
  public var projectedValue: Self { self }
}

// MARK: - Codable
extension Pack: Encodable where repeat each Element: Encodable {
  public func encode(to encoder: any Encoder) throws {
    var container = encoder.unkeyedContainer()
    repeat try container.encode(each wrappedValue)
  }
}

extension Pack: Decodable where repeat each Element: Decodable {
  public init(from decoder: any Decoder) throws {
    var container = try decoder.unkeyedContainer()
    wrappedValue = (repeat try container.decode((each Element).self))
  }
}
2 Likes

Because it’s not possible to encode/decode a parameter pack, it might work if the pack was a tuple(and the feature discussed here was implemented: Pitch: User-defined tuple conformances) or struct.

However, since parameter packs don’t seem to be either a type or a tuple, maybe it’s not possible yet. Do you know if this will be possible in the future? I didn’t see anything in the Generics Manifesto that suggests this.

1 Like

Thanks so much, it's just the decoding without knowing the types before hand, Generic parameter 'each Element' could not be inferred when not passing the type as Pack<Int, Bool, String>.self. I'm not sure how to specify this when it would change at run time.

I think you're trying to run before you walk. Swift's generics are compile-time constructs. Parameter packs can't help that.

To simplify, how would you go about instantiating this distillation of SandboxView from a DataWithType?

struct SandboxView<Option> { }

struct DataWithType: Codable {
  let data: Data
  let type: SupportedType
}

Exactly, I was wondering if there was some trick I didn't know about or if something was to be added to the language

In the end we found a work around, since this is a iOS and watchOS app that share a lot of code, we already have the pack with the types, we can just take the values that are sent to the watch and cast them as the types from the pack.

We create a new SandboxView on the watch that has the same init signature, this one is very stripped down that just shows the component but it matches String names we include in the pack to keys in the dictionary we send to the watch app.

/// Watch specific sandbox View
struct SandboxView<each Option, Content: View>: View {
  @Environment(WatchAppViewModel.self) private var watchAppViewModel

  var content: ((repeat each Option)) -> Content
  let state: (repeat (value: each Option, name: String))
  
  init(_ options: repeat (value: each Option, name: String),
       @ViewBuilder content: @escaping (repeat each Option) -> Content) {
    self.content = { (args: (repeat each Option)) in
      return content(.constant(.clear), repeat each args)
    }
    self.state = (repeat each initialState)
  }
  var body: some View {
    content((repeat each replaceState()))
  }
  /// Replace the state with values in the view model.
  func replaceState() -> (repeat each Option) {
    // Build values using any special case casting.
    func makeOptionValue<T>(_ originalValue: T, name: String) -> T {
      let newValue = watchAppViewModel.sandboxOptions.first(where: { $0.name == name })?.value
      if let newValue = newValue as? T {
        return newValue
      } else {
        return originalValue
      }
    }
    return (repeat makeOptionValue((each state).value, name: (each state).name))
  }
}

We have the makeOptionValue function inside replaceState to make it easier to think about one generic type instead of the whole pack.

One thing I don't understand is, if if let newValue = newValue as? T fails for one type all of the types fail and you get originalValue for every value.

1 Like