Member 'select' cannot be used on value of type 'any LayerCollection'

Consider the following piece of logic:

protocol LayerLike {} 
protocol LayerCollection {
  associatedtype Layer: LayerLike
  select(layer: Layer?)
}

extension LayerCollection {
  func select(layer: Layer?) {
    print(layer ?? "0")
  }
}

class LayerController {
  private var collections: [any LayerCollection] = []

  init() {
    layerCollections = [
      ConcreteLayerCollection(),
      MoreConreteLayerCollection()
    ]
  }

  func didSelect() {

    // 1
    layerCollections.forEach { $0.select(nil) } // FAIL

    // 2
    if let c = layerCollections.first as? ConcreteLayerCollection {
      c.select(nil) // SUCCESS
    }
  }
}

#1 throws the error:

Member 'select' cannot be used on value of type 'any LayerCollection'; consider using a generic constraint instead

Now I understand why this is happening. Calling select on 'any LayerCollection' loses the underlying type information at compile time. My concern is - how do I make this kind of logic work:

func didSelect() {
  layerCollections.forEach { layerCollection in
    if layerCollection.wasResponsible() {
      // layer = get layer from somewhere
      layerCollection.select(layer)
    }
  }
}

If I can't do the above, this array of layerCollections is useful for me and I wouldn't be able to support this kind of logic. What am I missing or what is a better way of doing this? Thanks for the help.

You cant feed anything from get layer from somewhere into LayerCollection.select even if the result type of that function is -> LayerLike;

What you're trying to do can probably be solved in another way but you should post more info or code;

1 Like

Alright, it wasn't easy to abstract the code below but I have tried my best. You can copy paste this into Xcode commenting out the text and it should fail at: layerCollection.select() at the end.

First, lets define a couple of protocols:

import Foundation
import UIKit

protocol LayerLike {
  var identifier: String { get }
}

protocol FeatureLike {}

Here I define a class LayerBinding which takes a generic type Layer and Feature; essentially wraps a layer.

class LayerBinding<Layer: LayerLike, Feature: FeatureLike> {
  var layer: Layer!

  init(layer: Layer) {
    self.layer = layer
  }
}

Here I create a protocol LayerCollection which describes two associated types Layer and Feature. A LayerCollection is basically a collection of layer bindings (that each wrap a layer).

struct CustomView {
  // implementation for customFeatures...
}

protocol LayerCollection {
  associatedtype Layer: LayerLike
  associatedtype Feature: FeatureLike

  var identifiers: [String] { get }
  var layerBindings: [LayerBinding<Layer, Feature>] { get }

  func configure() //works
  func features(atPoint point: CGPoint, view: CustomView) -> (Feature, Layer)? //works

  // concerning code
  func select(layer: Layer, feature: Feature)
}

I provide the default implementation for features API:

extension LayerCollection {
  var identifiers: [String] {
    get {
      self.layerBindings.map { $0.layer.identifier }
    }
  }

  func features(atPoint point: CGPoint, view: CustomView) -> (Feature, Layer)? {
    for layerBinding in layerBindings {
      let features = visibleFeatures(atPoint: point, layerBinding: layerBinding, view: view)
      if let features = features {
        for feature in features {
          return (feature, layerBinding.layer)
        }
      }
    }
    return nil
  }

  private func visibleFeatures(
    atPoint point: CGPoint,
    layerBinding: LayerBinding<Layer, Feature>,
    view: CustomView)
  -> [Feature]? {
    // here I would call customFeatures on CustomView and do some typecasting like below

    // let featureCast: (SomeTypeX) -> Feature? = { $0 as? Feature }
    // var features = view.customFeatures(at: point, ids: layerIds)
    // let genericFeatures = features.compactMap(featureCast)
    return nil
  }
}

Now lets create a concrete type: ConcreteLayerACollection:

class ConcreteFeature: FeatureLike {}
class ConcreteLayer: LayerLike {
  var identifier: String = "bla"
  init(v: Int) {}
}

final class ConcreteLayerACollection: LayerCollection {
  typealias Feature = ConcreteFeature
  typealias Layer = ConcreteLayer

  var layerBindings: [LayerBinding<Layer, Feature>] = []

  func configure() {
    self.layerBindings = [
      LayerBinding(layer: Layer(v: 5))
    ]
  }

  func select(layer: ConcreteLayer, feature: ConcreteFeature) {
    print(layer, feature)
  }
}

Now, there could be other concrete collections: ConcreteLayerXCollection, ConcreteLayerYCollection. For the sake of argument:

typealias ConcreteLayerXCollection = ConcreteLayerACollection
typealias ConcreteLayerYCollection = ConcreteLayerXCollection

Finally, there is a LayerController that's handling all these collections:

final class LayerController {
  private var layerCollections: [any LayerCollection] = []
  init() {
    layerCollections = [
      ConcreteLayerACollection(),
      ConcreteLayerXCollection(),
      ConcreteLayerYCollection(),
    ]
  }

  func someDelegateCallback1() {
    layerCollections.forEach { layerCollection in
      layerCollection.configure()
    }
  }

  func someDelegateCallback2BasedOnSomeTouchEvent(point: CGPoint, view: CustomView) {
    for layerCollection in layerCollections {
      if let (feature, layer) = layerCollection.features(atPoint: point, view: view) {
        // basically found a hit of sorts; here I basically say layerCollection.select()
        // However ERROR: Member 'select' cannot be used on value of type 'any LayerCollection'; consider using a generic constraint instead
        layerCollection.select(layer: layer, feature: feature)
      }
    }
  }
}

Thanks for your responses and your help.

Then it's as @anon9791410 suggested,

  func someDelegateCallback2BasedOnSomeTouchEvent(point: CGPoint, view: CustomView) {
    for layerCollection in layerCollections {
        func useItRetyped(_ lc: some LayerCollection) {
            if let (feature, layer) = lc.features(atPoint: point, view: view) {
              lc.select(layer: layer, feature: feature)
            }
        }
        useItRetyped(layerCollection)
    }
  }
1 Like

Oh! Can you explain why this works? I am a bit confused. :confounded:

    func showcaseUnsafeContext() {
        // step 1,
        // Creating a result whose underlying type and usage depends on what is "under the hood" of randomlyTypedLayerCollection at this point in time
        var randomlyTypedLayerCollection: any LayerCollection = ConcreteLayerACollection()
        let resultAtStep1 = randomlyTypedLayerCollection.features(atPoint: .zero, view: CustomView())!
        
        // ... step 2
        // Changing what is "under the hood" of randomlyTypedLayerCollection
        // I can change both the value + underlying type so this makes it unsafe for resultAtStep1's usage
        randomlyTypedLayerCollection = // some other type implementing LayerCollection
        // From the compiler's point of view whatever is there now is probably incompatible with what's inside resultAtStep1


        // step 3,
        // if this were allowed by the compiler then you would probably run into a runtime crash
        randomlyTypedLayerCollection.select(layer: resultAtStep1.1, feature: resultAtStep1.0)
    }
    
    func showcaseSafeContext() {
        var randomlyTypedLayerCollection: any LayerCollection = ConcreteLayerACollection()
        
        func useInSafeContext<T: LayerCollection>(_ t: T) {
            var staticallyTypedLayerCollection: T = t
            let result = staticallyTypedLayerCollection.features(atPoint: .zero, view: CustomView())!
            
            // now unless LayerCollection has a required .init()
            // there's no way for me here to assign to staticallyTypedLayerCollection a different value
             // staticallyTypedLayerCollection = // ... some other LayerCollection variable
            
            // and if LayerCollection had a required .init() then I could create new values
            // to assign to staticallyTypedLayerCollection
            
            staticallyTypedLayerCollection = T()
            
            // but even so, the underlying type & type constraints remain the same for that variable
            // because inside this function I can only change it's value and not the underlying type also
            
            // so this operation is safe
            staticallyTypedLayerCollection.select(layer: result.1, feature: result.0)
            
        }
    
     useInSafeContext(randomlyTypedLayerCollection)
    }

The example is extreme just for showcase purposes; Although I used a generic function in the safe context showcase the behaviour is the same subtuting it with some like in my previous post

This is very helpful and I understand quite clearly. Appreciate all the support.