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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Building a Custom Collection with Protocols in Swift
35 mins
- Getting Started
- Creating the Bag Struct
- Adding Edit Methods
- Adding Add Method
- Implementing the Remove Method
- Adopting Protocols
- Adopting CustomStringConvertible
- Creating Initializers
- Initializing Collections
- Understanding Custom Collections
- Enforcing Non-destructive Iteration
- Adopting the Sequence Protocol
- Conforming to Sequence
- Viewing Benefits of Sequence
- Improving Sequence
- Adopting the Collection Protocol
- Testing Your Collection
- Improving Collection
- Updating BagIndex
- Using Slices
- Where to Go From Here?
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:
- Define a new generic type,
BagIndex
. LikeBag
, this requires a generic type that'sHashable
for use with dictionaries. - 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. - 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:
- Replaces the
Index
type fromDictionaryIndex
toBagIndex
. - Creates a new
BagIndex
fromcontents
for bothstartIndex
andendIndex
. - Uses the
index
property ofBagIndex
to access and return an element fromcontents
. - Gets the
DictionaryIndex
value fromcontents
using the property ofBagIndex
and creates a newBagIndex
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:
- Create a fruit basket made up of four different fruits.
- 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 isSlice<Bag<String>>
. - Find the index of the least occurring fruit in those that remain.
- 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.
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!