Building a Custom Collection with Protocols in Swift

In this Swift tutorial, you’ll learn how to use collection protocols to create your own implementation of a Bag collection type. By Brody Eller.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Adopting Protocols

In Swift, a protocol defines a set of properties and methods that must be implemented in an object that adopts it. To adopt a protocol, simply add a colon after the definition of your class or struct followed by the name of the protocol you’d like to adopt. After you declare your adoption of the protocol, implement the required variables and methods on your object. Once complete, your object conforms to the protocol.

Note: You can learn more about protocols in our Protocol Oriented Programming tutorial.

Here’s an easy example. Currently, Bag objects expose little information on the results sidebar in the Playground.

Add the following code to the end of the playground (outside of the struct) to see Bag in action:

var shoppingCart = Bag<String>()
shoppingCart.add("Banana")
shoppingCart.add("Orange", occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")

Then press Command-Shift-Enter to execute the playground.

This creates a new Bag with a few pieces of fruit. If you look at the playground debugger, you’ll see the object type without any of its contents.

Bag prints its object name without listing its contents

Adopting CustomStringConvertible

Fortunately, Swift provides the CustomStringConvertible protocol for just this situation! Add the following just after the closing brace of Bag:

extension Bag: CustomStringConvertible {
  var description: String {
    return String(describing: contents)
  }
}

Conforming to CustomStringConvertible requires implementation of a single property named description. This property returns the textual representation of the specific instance.

This is where you would put any logic needed to create a string representing your data. Because Dictionary conforms to CustomStringConvertible, you simply delegate the description call to contents.

Press Command-Shift-Enter to run the playground again.

Take a look at the newly improved debug information for shoppingCart:

Bag now displays its contents in the playground debugger

Awesome! Now, as you add functionality to Bag, you’ll be able to verify its contents.

Great! You’re on your way as you create powerful collection types that feel native. Next up is initialization.

Creating Initializers

It’s pretty annoying that you have to add each element one at a time. You should be able to initialize your Bag by passing in a collection of objects to add.

Add the following code to the end of the playground (but notice that this will not compile just yet):

let dataArray = ["Banana", "Orange", "Banana"]
let dataDictionary = ["Banana": 2, "Orange": 1]
let dataSet: Set = ["Banana", "Orange", "Banana"]

var arrayBag = Bag(dataArray)
precondition(arrayBag.contents == dataDictionary,
  "Expected arrayBag contents to match \(dataDictionary)")

var dictionaryBag = Bag(dataDictionary)
precondition(dictionaryBag.contents == dataDictionary,
  "Expected dictionaryBag contents to match \(dataDictionary)")

var setBag = Bag(dataSet)
precondition(setBag.contents == ["Banana": 1, "Orange": 1],
  "Expected setBag contents to match \(["Banana": 1, "Orange": 1])")

This is how you might expect to create a Bag. But it won’t compile because you haven’t defined an initializer that takes other collections. Rather than explicitly creating an initialization method for each type, you’ll use generics.

Add the following methods just below totalCount inside the implementation of Bag:

// 1
init() { }

// 2
init<S: Sequence>(_ sequence: S) where
  S.Iterator.Element == Element {
  for element in sequence {
    add(element)
  }
}

// 3
init<S: Sequence>(_ sequence: S) where
  S.Iterator.Element == (key: Element, value: Int) {
  for (element, count) in sequence {
    add(element, occurrences: count)
  }
}

Here’s what you just added:

  1. First, you created an empty initializer. You’re required to add this when defining additional init methods.
  2. Next, you added an initializer that accepts anything that conforms to the Sequence protocol where the elements of that sequence are the same as the elements of the Bag. This covers both Array and Set types. You iterate over the passed in sequence and add each element one at a time.
  3. After this, you added a similar initializer but one that accepts tuples of type (Element, Int). An example of this is a Dictionary. Here, you iterate over each element in the sequence and add the specified count.

Press Command-Shift-Enter again to run the playground. Notice that the code you added at the bottom earlier now works.

Initializing Collections

These generic initializers enable a much wider variety of data sources for Bag objects. However, they do require you to first create the collection you pass into the initializer.

To avoid this, Swift supplies two protocols that enable initialization with sequence literals. Literals give you a shorthand way to write data without explicitly creating an object.

To see this, first add the following code to the end of your playground: (Note: This, too, will generate errors until you add the needed protocols.)

var arrayLiteralBag: Bag = ["Banana", "Orange", "Banana"]
precondition(arrayLiteralBag.contents == dataDictionary,
  "Expected arrayLiteralBag contents to match \(dataDictionary)")

var dictionaryLiteralBag: Bag = ["Banana": 2, "Orange": 1]
precondition(dictionaryLiteralBag.contents == dataDictionary,
  "Expected dictionaryLiteralBag contents to match \(dataDictionary)")

The code above is an example of initialization using Array and Dictionary literals rather than objects.

Now, to make these work, add the following two extensions just below the CustomStringConvertible extension:

// 1
extension Bag: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Element...) {
    self.init(elements)
  }
}

// 2
extension Bag: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (Element, Int)...) {
    self.init(elements.map { (key: $0.0, value: $0.1) })
  }
}
  1. ExpressibleByArrayLiteral is used to create a Bag from an array style literal. Here you use the initializer you created earlier and pass in the elements collection.
  2. ExpressibleByDictionaryLiteral does the same but for dictionary style literals. The map converts elements to the named-tuple the initializer expects.

With Bag looking a lot more like a native collection type, it’s time to get to the real magic.

Understanding Custom Collections

You’ve now learned enough to understand what a custom collection actually is: A collection object that you define that conforms to both the Sequence and Collection protocols.

In the last section, you defined an initializer that accepts collection objects conforming to the Sequence protocol. Sequence represents a type that provides sequential, iterated access to its elements. You can think of a sequence as a list of items that let you step over each element one at a time.

There are way too many Pokemon to keep track these days

A linked list is an example of a sequence

Iteration is a simple concept, but this ability provides huge functionality to your object. It allows you to perform a variety of powerful operations like:

  • map(_:): Returns an array of results after transforming each element in the sequence using the provided closure.
  • filter(_:): Returns an array of elements that satisfy the provided closure predicate.
  • sorted(by:): Returns an array of the elements in the sequence sorted based on the provided closure predicate.

This barely scratches the surface. To see all methods available from Sequence, take a look at Apple’s documentation on the Sequence Protocol.

Brody Eller

Contributors

Brody Eller

Author

David Sherline

Tech Editor

Steve Robertson

Editor

Luke Freeman

Illustrator

Matt Galloway

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.