13.
Iterator Pattern
Written by Jay Strawn
The iterator pattern is a behavioral pattern that provides a standard way to loop through a collection. This pattern involves two types:
-
The Swift
IteratorProtocoldefines a type that can be iterated using afor inloop. -
The iterator object is the type you want to make iterable. Instead of conforming to
IteratorProtocoldirectly, however, you can conform toSequence, which itself conforms toIteratorProtocol. By doing so, you’ll get many higher-order functions, includingmap,filterand more, for free.
What does “for free” mean? It means these useful built-in functions can be used on any object that conforms to Sequence, which can save you from writing your own sorting, splitting and comparing algorithms.
If you’re new to these functions, visit http://bit.ly/sequence-protocol to learn more about them.
When should you use it?
Use the iterator pattern when you have a type that holds onto a group of objects, and you want to make them iterable using a standard for in syntax.
Playground example
Open IntermediateDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace from the last chapter, then open the Iterator page.
You’ll be creating a queue in this example.
To quote the Swift Algorithm Club (http://bit.ly/swift-algorithm-club), “A queue is a list where you can only insert new items at the back and remove items from the front. This ensures that the first item you enqueue is also the first item you dequeue. First come, first serve!”
Add the following right after Code Example:
import Foundation
// 1
public struct Queue<T> {
private var array: [T?] = []
// 2
private var head = 0
// 3
public var isEmpty: Bool {
return count == 0
}
// 4
public var count: Int {
return array.count - head
}
// 5
public mutating func enqueue(_ element: T) {
array.append(element)
}
// 6
public mutating func dequeue() -> T? {
guard head < array.count,
let element = array[head] else {
return nil
}
array[head] = nil
head += 1
let percentage = Double(head)/Double(array.count)
if array.count > 50,
percentage > 0.25 {
array.removeFirst(head)
head = 0
}
return element
}
}
Here, you’ve created a queue containing an array. Here’s a breakdown of the code:
- You’ve defined that
Queuewill contain anarrayof any type. - The
headof the queue will be the index of the first element in the array. - There is an
isEmptybool to check if the queue is empty or not. - You’ve given
Queueacount. - You have created an
enqueuefunction for adding elements to the queue. - The
dequeuefunction is for removing the first element of the queue. This function’s logic is set up to help keep you from having nil objects in your array.
Next, add the following code to the end of the playground to test the queue:
public struct Ticket {
var description: String
var priority: PriorityType
enum PriorityType {
case low
case medium
case high
}
init(description: String, priority: PriorityType) {
self.description = description
self.priority = priority
}
}
var queue = Queue<Ticket>()
queue.enqueue(Ticket(
description: "Wireframe Tinder for dogs app",
priority: .low))
queue.enqueue(Ticket(
description: "Set up 4k monitor for Josh",
priority: .medium))
queue.enqueue(Ticket(
description: "There is smoke coming out of my laptop",
priority: .high))
queue.enqueue(Ticket(
description: "Put googly eyes on the Roomba",
priority: .low))
queue.dequeue()
The queue has four items, which becomes three once you’ve successfully dequeued the first ticket.
In a real use-case scenario, you’ll definitely want to be able to sort these tickets by priority. With the way things are now, you’d need to write a sorting function with a lot of if statements. Save yourself some time and instead use one of Swift’s built-in sorting functions.
Currently, if you attempt to use a for in loop or sorted() on queue, you’ll get an error. You need to make your Queue struct conform to the Sequence protocol. Add the following beneath your Queue struct:
extension Queue: Sequence {
public func makeIterator()
-> IndexingIterator<ArraySlice<T?>> {
let nonEmptyValues = array[head ..< array.count]
return nonEmptyValues.makeIterator()
}
}
Like with dequeue, you want to make sure you’re not exposing nil objects and only iterate through non-empty values.
There are two required parts when conforming the Sequence protocol. The first is your associated type, which is your Iterator. In the code above, IndexingIterator is your associated type, which is the default iterator for any collection that doesn’t declare its own.
The second part is the Iterator protocol, which is the required makeIterator function. It constructs an iterator for your class or struct.
Add the following to the bottom of the file:
print("List of Tickets in queue:")
for ticket in queue {
print(ticket?.description ?? "No Description")
}
This iterates through your tickets and prints them.
Before you use a sequence-specific sort function, scroll back up and add the following extension underneath the Ticket struct:
extension Ticket {
var sortIndex : Int {
switch self.priority {
case .low:
return 0
case .medium:
return 1
case .high:
return 2
}
}
}
Assigning numeric values to the priority levels will make sorting easier. Sort the tickets using their sortIndex as reference, add the following code at the end of the file:
let sortedTickets = queue.sorted {
$0!.sortIndex > ($1?.sortIndex)!
}
var sortedQueue = Queue<Ticket>()
for ticket in sortedTickets {
sortedQueue.enqueue(ticket!)
}
print("\n")
print("Tickets sorted by priority:")
for ticket in sortedQueue {
print(ticket?.description ?? "No Description")
}
The sorting function returns a regular array, so to have a sorted queue, you enqueue each array item into a new queue. The ability to sort through groups so easily is a powerful feature, and becomes more valuable as your lists and queues get larger.
What should you be careful about?
There is a protocol named IteratorProtocol, which allows you to customize how your object is iterated. You simply implement a next() method that returns the next object in the iteration. However, you’ll probably never need to conform to IteratorProtocol directly.
Even if you need a custom iterator, it’s almost always better to conform to Sequence and provide custom next() logic, instead of conforming to IteratorProtocol directly.
You can find more information about IteratorProtocol and how it works with Sequence at http://bit.ly/iterator-protocol.
Tutorial project
You’ll continue building onto Coffee Quest from the previous chapter. You’ll finally be adding functionality to the switch in the upper-right corner!
If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter\CoffeeQuest\CoffeeQuest.xcworkspace (not .xcodeproj!) in Xcode.
Note: If you opt to start fresh, then you’ll need to open up APIKeys.swift and add your Yelp API key. See Chapter 10, “Model-View-ViewModel Pattern” for instructions on how to generate this.
Go to the Models group and select File ▸ New ▸ File… and choose Swift File. Name the new file Filter.swift. Create a Filter struct by adding the following code underneath import Foundation:
public struct Filter {
public let filter: (Business) -> Bool
public var businesses: [Business]
public static func identity() -> Filter {
return Filter(filter: { _ in return true }, businesses: [])
}
public static func starRating(
atLeast starRating: Double) -> Filter {
return Filter(filter: { $0.rating >= starRating },
businesses: [])
}
public func filterBusinesses() -> [Business] {
return businesses.filter (filter)
}
}
extension Filter: Sequence {
public func makeIterator() -> IndexingIterator<[Business]> {
return filterBusinesses().makeIterator()
}
}
This struct holds an array of Business objects and a filter closure. You can instantiate the class with identity(), adjust the filter’s parameters with starRating(), and apply the filter with filterBusinesses().
You also make Filter conform to Sequence via an extentension, wherein you create an iterator from the return value of filterBusinesses().
With your filter wrapper set up, you can now use this logic in the ViewController. Open ViewController.swift. Add the following line of code to the list of properties at the top:
private var filter = Filter.identity()
This creates a new property for filter and sets its default value to Filter.identity().
Next, in the searchForBusinesses function, add the following right underneath the line that reads self.businesses = searchResult.businesses and above the line with DispatchQueue.main.async:
self.filter.businesses = businesses
Here you set filter.businesses to the fetched businesses.
Next, you’ll set the filter based on the top-right switch. Add the following code inside businessFilterToggleChanged(_:):
if sender.isOn {
// 1
filter = Filter.starRating(atLeast: 4.0)
} else {
// 2
filter = Filter.identity()
}
// 3
filter.businesses = businesses
// 4
addAnnotations()
Here’s how this works line by line:
-
If the switch is on, you set
filtertoFilter.starRating(atLeast: 4.0). This will only show coffee shops that are rated4.0or better. -
If the switch isn’t on, you set
filtertoFilter.identity(). This will show all coffee shops. -
In either case, you set
filter.businessesto the existingbusinessesthat were previously fetched. -
Lastly, you call
addAnnotations()to update the map.
You also need to actually use the filter whenever you update the map. Replace the contents of addAnnotations() with the following:
// 1
mapView.removeAnnotations(mapView.annotations)
// 2
for business in filter {
// 3
let viewModel =
annotationFactory.createBusinessMapViewModel(for: business)
mapView.addAnnotation(viewModel)
}
Here’s how this works:
-
You first remove the existing annotations from the map. This prevents duplicates from being shown, which is possible because this method is called whenever the user toggles the switch.
-
You loop through each
businessinfilter. Under the hood, this callsmakeIterator()onFilter, which callsfilterBusinesses().makeIterator, and this is what actually filters the businesses. -
You create a
viewModelfor eachbusinessand add this to the map.
Build and run the app, and try toggling the switch in the top-right corner a few times. When it’s turned on, only highly-rated coffee shops will be shown. When it’s off, all of the coffee shops will be shown.
Key points
You learned about the iterator pattern in this chapter. Here are its key points:
-
The iterator pattern provides a standard way to loop through a collection using a
for insyntax. -
It’s better to make your custom objects conform to
Sequence, instead ofIteratorProtocoldirectly. -
By conforming to
Sequence, you will get higher-order functions likemapandfilterfor free.
Where to go from here?
You’ve added a lot of great functionality to Coffee Question over the last few chapters! However, there’s still many more features you could add:
- Advanced filtering and searching options
- Custom address input instead of just searching nearby
- Saving and displaying favorited coffee shops
Each of these are possible using the existing patterns you’ve learned so far. Feel free to continue building out Coffee Quest as much as you like.
When you’re ready, continue onto the next chapter to learn about the prototype design pattern and build a new example app.