Getting started with associated types in Swift Protocols

Associated types in Swift work closely together with protocols. You can literally see them as an associated type of a protocol: they are family from the moment you put them together.

Obviously, it’s a bit more complicated to explain how associated types work but once you get the hang of it, you’ll be using them more often. Protocols allow us to define a common interface between types

What is an associated type?

An associated type can be seen as a replacement of a specific type within a protocol definition. In other words: it’s a placeholder name of a type to use until the protocol is adopted and the exact type is specified.

This is best explained by a simple code example. Without an associated type, the following protocol would only work for the type we define. In this case: Strings.

protocol StringsCollection {
    var count: Int { get }
    subscript(index: Int) -> String { get }
    mutating func append(_ item: String)
}

If we want to make use of the same logic for a collection of doubles, we would need to recreate a new protocol. Associated types prevent this by putting in a placeholder item:

protocol Collection {
    associatedtype Item
    var count: Int { get }
    subscript(index: Int) -> Item { get }
    mutating func append(_ item: Item)
}

The associated type is defined using the associatedtype keyword and tells the protocol that the subscript return type equals the append item type. This way we allow the protocol to be used with any associated type later defined. An example implementation could look as follows:

struct UppercaseStringsCollection: Collection {
    
    var container: [String] = []
    
    var count: Int { container.count }
    
    mutating func append(_ item: String) {
        guard !container.contains(item) else { return }
        container.append(item.uppercased())
    }
    
    subscript(index: Int) -> String {
        return container[index]
    }
}

What are the benefits of using associated types?

The benefits of an associated type should become visible once you start working with them. They prevent writing duplicate code by making it easier to define a common interface for multiple scenarios. This way, the same logic can be reused for multiple different types, allowing you to write and test logic only once.

A useful example of associated types in action

I always like to demonstrate code techniques with a real case example. In the Collect by WeTransfer app we had to make use of brand colors for our new design. The defined colors had to be available for both UIColor in UIKit and Color in SwiftUI. Obviously, we did not want to define all colors twice as that would be hard to maintain and result in a lot of duplicate code.

We already had a convenient way of defining colors using a custom convenience initialiser on UIColor that takes a hexadecimal input:

let color = UIColor(hex: "FF7217")

We decided to reuse that method and define a protocol around it:

public protocol BrandColorSupporting {
    associatedtype ColorValue
    static func colorFor(hex: String, alpha: CGFloat) -> ColorValue
}

Some colors also required to be defined with a specific alpha value which we included in this protocol too. We then added support for this protocol for both UIColor and Color:

extension UIColor: BrandColorSupporting {
    public static func colorFor(hex: String, alpha: CGFloat) -> UIColor {
        return UIColor(hex: hex).withAlphaComponent(alpha)
    }
}

@available(iOS 13.0, *)
extension Color: BrandColorSupporting {
    public static func colorFor(hex: String, alpha: CGFloat) -> Color {
        return Color(UIColor.colorFor(hex: hex, alpha: alpha))
    }
}

As you can see, we reuse the logic of UIColor in our Color protocol adoption. This allowed us to reuse the convenience initialiser of UIColor to create colors using a hexadecimal value.

As not all colors required to define an alpha component we added a default extension to our BrandColorSupporting protocol:

extension BrandColorSupporting {
    static func colorFor(hex: String) -> ColorValue {
        return colorFor(hex: hex, alpha: 1.0)
    }
}

Defining static colors

Both UIColor and Color now conform to the BrandColorSupporting protocol which means that we can define extensions that become available to both.

We started defining colors:

public extension BrandColorSupporting {

    static var orangeCollectHero: ColorValue {
        colorFor(hex: "FF7217")
    }
}

The best thing of using associated types is that we can make use of the same logic while the result type is changed based on context:

let colorForSwiftUI: Color = Color.orangeCollectHero
let colorForUIKit: UIColor = UIColor.orangeCollectHero

As you can imagine this is a great way to work with brand colors. We’re already prepared for Integrating SwiftUI with UIKit apps for early adoption without having to redefine all our colors.

Adding protocol constraints to an Associated Type

Now that we’ve seen how an associated type could work in a real case example it’s time to dive a bit deeper into the available possibilities.

Let’s continue with our example showed earlier for defining a Collection:

protocol Collection {
    associatedtype Item
    var count: Int { get }
    subscript(index: Int) -> Item { get }
    mutating func append(_ item: Item)
}

What if we want to compare the appending item to any of the existing items before inserting? We need to have a way to compare for equity.

We can do this by constraining the associated type to, in this case, the Equatable protocol:

protocol Collection {
    // The associated type now needs to conform to Equatable
    associatedtype Item: Equatable

    var count: Int { get }
    subscript(index: Int) -> Item { get }
    mutating func append(_ item: Item)
}

Conforming a protocol to a protocol with a defined Associated Type

In some cases you want to conform a protocol to another protocol that defines an associated type.

protocol CollectionSlice: Collection {
    func prefix(_ maxLength: Int) -> CollectionSlice
}

It’s not uncommon to run into an error like this:

Protocol ‘CollectionSlice’ can only be used as a generic constraint because it has Self or associated type requirements

This is because the compiler can’t make sure that the returned CollectionSlice will result in the same underlying associated type as the defined protocol. Therefore, we need to setup a constraint that makes sure both types are equal:

protocol CollectionSlice: Collection {
    associatedtype Slice: CollectionSlice where Slice.Item == Item
    func prefix(_ maxLength: Int) -> Slice
}

Implementors of this protocol are now required to return a slice of the same type as its parent collection:

extension UppercaseStringsCollection: CollectionSlice {
    func prefix(_ maxLength: Int) -> UppercaseStringsCollection {
        var collection = UppercaseStringsCollection()
        for index in 0..<min(maxLength, count) {
            collection.append(self[index])
        }
        return collection
    }
}

Conclusion

Associated Types in Swift are a powerful way to define code that can be reused among multiple different types. Practice is required to get the most out of it but once you understand the principle you can easily reuse a lot of code.

If you like to learn more tips on Swift, check out the swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!