Generic type constraint at public init() prevents hiding of internal implementation detail type I want to hide, is there anyway to overcome this?

In the code below, I want to hide the IdentifiableIndices type but cannot because this type is used as generic constraint of my public init:

import SwiftUI

// vvvvvvvvv cannot be fileprivate because at the bottom this type is use as generic constrain of my public init
/*fileprivate*/struct IdentifiableIndices<Base> where Base: RandomAccessCollection, Base.Element: Identifiable {

    typealias Index = Base.Index

    struct Element: Identifiable {
        let id: Base.Element.ID
        let rawValue: Index

        func callAsFunction() -> Index { rawValue }
    }

    fileprivate var base: Base
}

extension IdentifiableIndices: RandomAccessCollection {
    var startIndex: Index { base.startIndex }
    var endIndex: Index { base.endIndex }

    subscript(position: Index) -> Element {
        Element(id: base[position].id, rawValue: position)
    }

    func index(before index: Index) -> Index {
        base.index(before: index)
    }

    func index(after index: Index) -> Index {
        base.index(after: index)
    }
}


extension ForEach where ID == Data.Element.ID, Data.Element: Identifiable, Content: View {
    // vvvvv no can do!                             and this prevents **fileprivate IdentifiableIndices**:   vvvvvvvvvvvv
    /*public*/init<T>(indicesOf data: T, @ViewBuilder content: @escaping (T.Index) -> Content) where Data == IdentifiableIndices<T> {
        self.init(IdentifiableIndices(base: data)) { index in
            content(index())
        }
    }
}

I don't think you can make IdentifiableIndices private. It's true that it's an implementation detail, but when you initialize your ForEach with an IdentifiableIndices instance, you get an instance of ForEach<IdentifiableIndices<Base>, ...> which cannot be used in a public environment.
You would get an error like:

Constant must be declared private or fileprivate because its type 'ForEach<...>' uses a private type

Do you intend to make that initializer private too?

when you initialize your ForEach with an IdentifiableIndices instance,

at the call site, it's just RandomAccessCollection, there is no IdentifiableIndices, that's created internally:

func contentView(index: Array.Index) { ... }
...
ForEach(indicesOf: someArray, content: contentView(index:))

so the caller don't need to know about IdentifiableIndices, so I want to hide it if it's possible.

But since the result is a ForEach view of <IdentifiableIndices> so due to this then there is no way to make IdentifiableIndices private.

this doesn’t make sense. how would the caller of init<T>(indicesOf:content:) know if T is a valid Base?

I don't know how the user would know what T is. But is the same as the built-in ForEach, only it iterate the collection provide index instead of element to the content view closure. All the types are known to the caller.

The signature of this ForEach.init do not have IdentifiableIndices<T> at all. This is why do don't want to expose this type.

T is in the signature of your public init<T>(indicesOf:content:). if you call it from another module, how does the type checker know if it satisfies the where Data == IdentifiableIndices<T> constraint, if it doesn’t know about IdentifiableIndices<U>?

As I said earlier, you're right that your IdentifiableIndices<Base> is an implementation detail, in the sense that the user will never explicitly initialize an instance of that type. However, "implementation detail" and "private" are two different concepts. In order to have a type to be private, you need to ensure that that type won't surface in any type signature and it's usually achieved by means of type erasure (either using an existential box or an opaque type).

Let me change a little bit your initializer and let's suppose you can use a static function instead — let's call it ForEach.index(of:) — having an opaque return type some View:

extension ForEach
where Data.Element: Identifiable, ID == Data.Element.ID, Content: View {
  public static func index(
    of data: Data,
    @ViewBuilder content: @escaping (Data.Index) -> Content
  ) -> some View {
    ForEach<IdentifiableIndices<Data>, Data.Element.ID, Content>(
      IdentifiableIndices(base: data)
    ) { index in
      content(index())
    }
  }
}

struct MyView: View {
  var body: some View {
    ForEach.index(of: data) { index in
      Text("\(index)")
    }
  }
}

It should work even with a private IdentifiableIndices<Base>. However, if you don't use an opaque type, the function signature becomes

func index(
  of data: Data,
  @ViewBuilder content: @escaping (Data.Index) -> Content
) -> ForEach<IdentifiableIndices<Data>, Data.Element.ID, Content>

which cannot be exposed as public since it has a private type in it. You'd get

Method cannot be declared public because its result uses a private type

With an initializer, i.e. what you have now, there's no way to erase the return type, so, even if your type is implementation detail, you need to keep it public.


Side note: do you really need to make IdentifiableIndices<Base> private?
If you follow SwiftUI's path, you'll see that implementation detail views are all public. As an example, if I recall correctly, originally SwiftUI had _ModifiedContent with a leading _ (the view used when you apply a modifier to a view). Now it has been renamed to just ModifiedContent.

1 Like

No, not really. I'm trying to learn what's possible with generic. Your explanation is great! I was hoping for something like "opaque type" at the init type constraints to hide the IdentifiableIndices<Base> type. Now I understand this is only possible with method and not init.

:+1::pray:

Terms of Service

Privacy Policy

Cookie Policy