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 4 of 4 of this article. Click here to view the first page.

Testing Your Collection

Add the following code to the end of the playground to test some of the new functionality:

// Get the first item in the bag
let firstItem = shoppingCart.first
precondition(
  (firstItem!.element == "Orange" && firstItem!.count == 1) ||
  (firstItem?.element == "Banana" && firstItem?.count == 2),
  "Expected first item of shopping cart to be (\"Orange\", 1) or (\"Banana\", 2)")

// Check if the bag is empty
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false,
  "Expected shopping cart to not be empty")

// Get the number of unique items in the bag
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2,
  "Expected shoppingCart to have 2 unique items")

// Find the first item with an element of "Banana"
let bananaIndex = shoppingCart.indices.first { 
  shoppingCart[$0].element == "Banana"
}!
let banana = shoppingCart[bananaIndex]
precondition(banana.element == "Banana" && banana.count == 2,
  "Expected banana to have value (\"Banana\", 2)")

Once again, run the playground. Awesome!

Cue the moment where you're feeling pretty good about what you've done, but sense that a "but wait, you can do better" comment is coming... Well, you're right! You can do better. There's still some Dictionary smell leaking from your Bag.

Improving Collection

Bag is back to showing too much of its inner workings. Users of Bag need to use DictionaryIndex objects to access elements within the collection.

You can easily fix this. Add the following after the Collection extension:

// 1
struct BagIndex<Element: Hashable> {
  // 2
  fileprivate let index: DictionaryIndex<Element, Int>

  // 3
  fileprivate init(
    _ dictionaryIndex: DictionaryIndex<Element, Int>) {
    self.index = dictionaryIndex
  }
}

In the code above, you:

  1. Define a new generic type, BagIndex. Like Bag, this requires a generic type that's Hashable for use with dictionaries.
  2. Make the underlying data for this index type a DictionaryIndex object. BagIndex is really just a wrapper that hides its true index from the outside world.
  3. Create an initializer that accepts a DictionaryIndex to store.

Now you need to think about the fact that Collection requires Index to be comparable to allow comparing two indexes to perform operations. Because of this, BagIndex needs to adopt Comparable.

Add the following extension just after BagIndex:

extension BagIndex: Comparable {
  static func ==(lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index == rhs.index
  }

  static func <(lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index < rhs.index
  }
}

The logic here is simple; you're using the equivalent methods of DictionaryIndex to return the correct value.

Updating BagIndex

Now you're ready to update Bag to use BagIndex. Replace the Collection extension with the following:

extension Bag: Collection {
  // 1
  typealias Index = BagIndex<Element>

  var startIndex: Index {
    // 2.1
    return BagIndex(contents.startIndex)
  }

  var endIndex: Index {
    // 2.2
    return BagIndex(contents.endIndex)
  }

  subscript (position: Index) -> Iterator.Element {
    precondition((startIndex ..< endIndex).contains(position),
      "out of bounds")
    // 3
    let dictionaryElement = contents[position.index]
    return (element: dictionaryElement.key,
      count: dictionaryElement.value)
  }

  func index(after i: Index) -> Index {
    // 4
    return Index(contents.index(after: i.index))
  }
}

Each numbered comment marks a change. Here's what they are:

  1. Replaces the Index type from DictionaryIndex to BagIndex.
  2. Creates a new BagIndex from contents for both startIndex and endIndex.
  3. Uses the index property of BagIndex to access and return an element from contents.
  4. Gets the DictionaryIndex value from contents using the property of BagIndex and creates a new BagIndex using this value.

That's it! Users are back to knowing nothing about how you store the data. You also have the potential for much greater control of index objects.

Before wrapping this up, there's one more important topic to cover. With the addition of index-based access, you can now index a range of values in a collection. Time for you to take a look at how a slice works with collections.

Using Slices

A slice is a view into a subsequence of elements within a collection. It lets you perform actions on a specific subsequence of elements without making a copy.

A slice stores a reference to the base collection you create it from. Slices share indices with their base collection, keeping references to the start and end indices to mark the subsequence range. Slices have an O(1) space complexity because they directly reference their base collection.

To see how this works, add the following code to the end of the playground:

// 1
let fruitBasket = Bag(dictionaryLiteral:
  ("Apple", 5), ("Orange", 2), ("Pear", 3), ("Banana", 7))

// 2
let fruitSlice = fruitBasket.dropFirst()

// 3
if let fruitMinIndex = fruitSlice.indices.min(by:
  { fruitSlice[$0] > fruitSlice[$1] }) {
  // 4
  let basketElement = fruitBasket[fruitMinIndex]
  let sliceElement = fruitSlice[fruitMinIndex]
  precondition(basketElement == sliceElement,
    "Expected basketElement and sliceElement to be the same element")
}

Run the playground again.

In the code above, you:

  1. Create a fruit basket made up of four different fruits.
  2. Remove the first type of fruit. This actually just creates a new slice view into the fruit basket excluding the first element you removed, instead of creating a whole new Bag object. You'll notice in the results bar that the type here is Slice<Bag<String>>.
  3. Find the index of the least occurring fruit in those that remain.
  4. Prove that you're able to use the index from both the base collection as well as the slice to retrieve the same element, even though you calculated the index from the slice.
Note: Slices may seem a little less useful for hash-based collections like Dictionary and Bag because their order isn't defined in any meaningful way. An Array, on the other hand, is an excellent example of a collection type where slices play a huge role in performing subsequence operations.

Congratulations — you're now a collection pro! You can celebrate by filling your Bag with your own custom prizes. :]

Where to Go From Here?

You can download the complete playground with all the code in this tutorial using the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned how to make a custom collection in Swift. You added conformance to Sequence, Collection, CustomStringConvertible, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral and you created your own index type.

If you'd like to view or contribute to a more complete Bag implementation, check out the Swift Algorithm Club implementation as well as the Foundation implementation, NSCountedSet.

These are just a taste of all the protocols Swift provides to create robust and useful collection types. If you'd like to read about some not covered here, check out the following:

You can also check out more information about Protocols in Swift and learn more about adopting common protocols available in the Swift standard library.

Finally, be sure to read our article on Protocol-Oriented Programming in Swift!

I hope you enjoyed this tutorial! Building your own custom collection definitely comes in handy, and gives you a better understanding of Swift's standard collection types.

If you have any comments or questions, feel free to join in the forum discussion below!

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.