Patterns for Working With Associated Types

Hiding Behind Protocols

Hiding Behind Protocols

This is an especially useful and flexible pattern. It can be used in many situations where you want to use protocols with associated types like a normal, full fledged type, but still be able to opt in to the generic part if necessary. The idea here is that you define two protocols that share common methods. Only one of those protocols contains associated types, the other does not. Your types conform to both protocols. This means that you can use the normal protocol as a type for all situations. If you, then, need to use the parts of the type that only affect the associated type, you can do so by means of a runtime cast.

Begin by defining an associated Protocol ExampleAssociatedProtocol that is shadowed by a normal Protocol ExampleProtocol.

/// The `Normal` Protocol
protocol ExampleProtocol {
  var anyValue: Any { get }
}

/// The Protocol with an associated type
protocol ExampleAssociatedProtocol: ExampleProtocol {
  associatedtype Value

  /// Retrieving the actual associated type
  var value: Value { get }
}

/// Conform to the `ExampleProtocol`
extension ExampleAssociatedProtocol {
  var anyValue: Any {
    return value
  }
}

Now, you can use the ExampleProtocol as a normal type throughout your app in all situations where a protocol with an associated type would otherwise fail:

struct World {
  var examples: [ExampleProtocol]

  let example: ExampleProtocol

  func generate() -> ExampleProtocol { 
    return example
  }
}

However, if you need to access the property that is specific to the ExampleAssociatedProtocol (value) then you can do so through at runtime.

/// Custom type implementing `ExampleAssociatedProtocol`
struct IntExample: ExampleAssociatedProtocol {
  var value: Int
}

/// Custom type implementing `ExampleAssociatedProtocol`
struct StringExample: ExampleAssociatedProtocol {
  var value: String
}

/// Shadowing via `ExampleProtocol`
let myExamples: [ExampleProtocol] = 
    [StringExample(value: "A"), IntExample(value: 10)]

/// Runtime Casting
for aNormalExample in myExamples {
  if let anAssociatedExample = aNormalExample as? IntExample {
    print(anAssociatedExample.value)
  }
  if let anAssociatedExample = aNormalExample as? StringExample {
    print(anAssociatedExample.value)
  }
}

This will print "A10" as both types (IntExample and StringExample) are being identified at runtime via a cast from ExampleProtocol.