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?
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.
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.
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
:
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:
- First, you created an empty initializer. You’re required to add this when defining additional
init
methods. - 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 theBag
. This covers bothArray
andSet
types. You iterate over the passed in sequence and add each element one at a time. - After this, you added a similar initializer but one that accepts tuples of type
(Element, Int)
. An example of this is aDictionary
. 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) })
}
}
-
ExpressibleByArrayLiteral
is used to create aBag
from an array style literal. Here you use the initializer you created earlier and pass in theelements
collection. -
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.
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.