Introduction to Swift Collections

Welcome back, aspiring Swift developer! So far, we’ve learned how to store individual pieces of information using variables and constants, and how to make decisions using control flow. But what if you need to store many pieces of information that are related? Imagine you’re building a shopping list, a contact book, or a list of high scores for a game. Storing each item in a separate variable would be incredibly tedious and inefficient!

This is where collections come in! In Swift, collections are powerful tools that allow you to store multiple values in a single, organized structure. They are fundamental to almost every application you’ll ever build, helping you manage lists of items, associate data with specific labels, and keep track of unique elements.

In this chapter, we’ll dive deep into Swift’s three primary collection types:

  1. Arrays: Ordered lists of values.
  2. Dictionaries: Unordered collections of key-value pairs.
  3. Sets: Unordered collections of unique values.

We’ll explore how to create, access, modify, and iterate over each of these collections, focusing on best practices and understanding why each collection type exists and when to use it. By the end of this chapter, you’ll have a solid grasp of how to manage groups of data effectively in Swift, a crucial skill for building robust iOS applications.

Ready to organize your data like a pro? Let’s begin!

Core Concepts: Understanding Swift’s Collection Types

Before we get our hands dirty with code, let’s understand the fundamental characteristics of Swift’s collections.

At their heart, Swift’s collections are designed to be type-safe. This means that when you create a collection, Swift expects all the items within it to be of the same type. For example, an Array of Strings can only hold String values. This helps prevent common programming errors and makes your code more predictable.

Another important characteristic is that Swift’s standard library collections (Arrays, Dictionaries, Sets) are value types. This means when you assign a collection to a new variable or pass it to a function, a copy of that collection is made. Any changes to the new copy won’t affect the original. This behavior is often desired and helps prevent unexpected side effects in your code.

Let’s break down each collection type.

Arrays: The Ordered List

Imagine a numbered list of your favorite movies or a queue of people waiting in line. That’s essentially what an Array is in Swift.

  • Ordered: The order in which you add items is preserved. Each item has an index (a numerical position) starting from 0.
  • Allows Duplicates: You can have the same value appear multiple times in an array.
  • Type-Safe: All elements must be of the same type.

Arrays are incredibly common for storing sequences of data where the order matters.

Creating Arrays

There are several ways to create arrays:

  1. Array Literal: The simplest way, using square brackets [] with comma-separated values. Swift can usually infer the type.

    // Swift infers this is an Array of Strings ([String])
    let favoriteFruits = ["Apple", "Banana", "Mango"]
    print(favoriteFruits)
    
  2. Explicit Type Annotation: If you want to be clear or if the array is empty initially, you can explicitly state its type.

    // An empty array of Strings
    var shoppingList: [String] = []
    print("My shopping list is currently: \(shoppingList)")
    
    // An array of Integers
    let lotteryNumbers: [Int] = [4, 8, 15, 16, 23, 42]
    print("Today's lucky numbers: \(lotteryNumbers)")
    
  3. Using an Initializer with Default Values: To create an array with a specific number of repeated values.

    // Creates an array of 3 'false' Boolean values
    let threeBooleans = Array(repeating: false, count: 3)
    print(threeBooleans) // Output: [false, false, false]
    

Accessing Array Elements

You access elements using their zero-based index.

let colors = ["Red", "Green", "Blue"]

// Accessing the first element (index 0)
let firstColor = colors[0]
print("The first color is: \(firstColor)")

// Accessing the last element (index 2)
let lastColor = colors[2]
print("The last color is: \(lastColor)")

Important: Trying to access an index that doesn’t exist will cause a runtime error (your app will crash!). Always be careful with indices.

Swift also provides handy properties for safe access:

let moreColors = ["Red", "Green", "Blue"]

// Get the number of items in the array
let numberOfColors = moreColors.count
print("There are \(numberOfColors) colors.")

// Check if the array is empty
let isEmpty = moreColors.isEmpty
print("Is the array empty? \(isEmpty)")

// Safely get the first and last elements (returns an Optional!)
if let first = moreColors.first {
    print("First color (safe): \(first)")
}

if let last = moreColors.last {
    print("Last color (safe): \(last)")
}

Modifying Arrays

Arrays are mutable if declared with var.

var toDoList = ["Buy groceries", "Walk the dog", "Finish Swift chapter"]
print("Initial To-Do List: \(toDoList)")

// 1. Add a new item to the end
toDoList.append("Call mom")
print("After appending: \(toDoList)")

// 2. Insert an item at a specific index
toDoList.insert("Plan vacation", at: 1) // Inserts at index 1, shifting others
print("After inserting: \(toDoList)")

// 3. Change an item at a specific index
toDoList[0] = "Buy organic groceries"
print("After changing: \(toDoList)")

// 4. Remove an item at a specific index
let removedItem = toDoList.remove(at: 2) // Removes "Walk the dog"
print("Removed item: \(removedItem)")
print("After removing: \(toDoList)")

// 5. Remove the last item
let lastRemoved = toDoList.removeLast()
print("Removed last item: \(lastRemoved)")
print("After removing last: \(toDoList)")

// 6. Remove all items
toDoList.removeAll()
print("After removing all: \(toDoList)")
print("Is To-Do List empty now? \(toDoList.isEmpty)")

Iterating Over Arrays

You can loop through all elements in an array using a for-in loop.

let guests = ["Alice", "Bob", "Charlie"]

print("Guest List:")
for guest in guests {
    print("- \(guest)")
}

// What if you also need the index? Use `enumerated()`
print("\nGuest List with Index:")
for (index, guest) in guests.enumerated() {
    print("\(index + 1). \(guest)")
}

Dictionaries: Key-Value Pairs

Think of a real-world dictionary where you look up a word (the “key”) to find its definition (the “value”). Or a phone book where a person’s name (key) maps to their phone number (value). That’s a Dictionary in Swift.

  • Unordered: The order of items is not guaranteed. You can’t rely on elements being in a specific sequence.
  • Key-Value Pairs: Each item consists of a unique key and an associated value.
  • Unique Keys: Every key in a dictionary must be unique. If you try to add a new value with an existing key, it will overwrite the old value.
  • Type-Safe: All keys must be of the same type, and all values must be of the same type. Keys must also conform to the Hashable protocol (which most standard Swift types like String, Int, Double, Bool already do).

Dictionaries are perfect for storing data where you need to quickly retrieve a value based on a specific identifier.

Creating Dictionaries

  1. Dictionary Literal: Using square brackets [] with key: value pairs.

    // Swift infers this is a Dictionary of [String: String]
    let countryCapitals = ["USA": "Washington D.C.", "France": "Paris", "Japan": "Tokyo"]
    print(countryCapitals)
    
  2. Explicit Type Annotation: For clarity or empty dictionaries.

    // An empty dictionary where keys are Strings and values are Ints
    var scores: [String: Int] = [:]
    print("Initial scores: \(scores)")
    
    // A dictionary mapping product IDs (Int) to product names (String)
    let productCatalog: [Int: String] = [
        101: "Laptop",
        203: "Mouse",
        512: "Keyboard"
    ]
    print("Product Catalog: \(productCatalog)")
    

Accessing Dictionary Values

You access values using their associated keys. The result is always an Optional because the key might not exist in the dictionary.

let userProfiles = ["Alice": 30, "Bob": 24, "Charlie": 35]

// Accessing Alice's age
let aliceAge = userProfiles["Alice"] // Type is Int? (Optional Int)
print("Alice's age: \(aliceAge)") // Output: Optional(30)

// Safely unwrapping Alice's age
if let age = userProfiles["Alice"] {
    print("Alice is \(age) years old.")
} else {
    print("Alice's profile not found.")
}

// Trying to access a non-existent key
let davidAge = userProfiles["David"]
print("David's age: \(davidAge)") // Output: nil

Modifying Dictionaries

Dictionaries are mutable if declared with var.

var userSettings = ["theme": "dark", "notifications": "on", "language": "en"]
print("Initial settings: \(userSettings)")

// 1. Add a new key-value pair
userSettings["fontSize"] = "medium"
print("After adding fontSize: \(userSettings)")

// 2. Update an existing value
userSettings["theme"] = "light"
print("After updating theme: \(userSettings)")

// 3. Using `updateValue(forKey:)` - returns the *old* value as an Optional
if let oldValue = userSettings.updateValue("fr", forKey: "language") {
    print("The old language was: \(oldValue)")
}
print("After updating language with `updateValue`: \(userSettings)")

// 4. Remove a key-value pair by assigning `nil` to the key
userSettings["notifications"] = nil
print("After removing notifications: \(userSettings)")

// 5. Using `removeValue(forKey:)` - returns the *removed* value as an Optional
if let removedFontSize = userSettings.removeValue(forKey: "fontSize") {
    print("Removed font size: \(removedFontSize)")
}
print("After removing fontSize with `removeValue`: \(userSettings)")

// 6. Remove all items
userSettings.removeAll()
print("After removing all settings: \(userSettings)")
print("Is userSettings empty now? \(userSettings.isEmpty)")

Iterating Over Dictionaries

When you iterate over a dictionary, you get each key-value pair as a tuple.

let studentGrades = ["Math": 95, "Science": 88, "History": 72]

print("Student Grades:")
for (subject, grade) in studentGrades {
    print("- \(subject): \(grade)")
}

// You can also iterate over just the keys or just the values
print("\nSubjects:")
for subject in studentGrades.keys {
    print("  \(subject)")
}

print("\nGrades:")
for grade in studentGrades.values {
    print("  \(grade)")
}

Sets: The Collection of Unique Items

Imagine a list of unique tags for a blog post, or a group of attendees at an event where each person should only be counted once. This is where Sets shine.

  • Unordered: Like Dictionaries, the order of items in a Set is not guaranteed.
  • Unique Values: A Set only stores distinct values. If you try to add an item that’s already in the set, it will be ignored.
  • Type-Safe: All elements must be of the same type and must conform to the Hashable protocol.

Sets are ideal for situations where you need to ensure every item is unique and the order doesn’t matter. They are also excellent for performing mathematical set operations like unions and intersections.

Creating Sets

  1. From an Array Literal (with type annotation): You typically create a Set from an array literal, but you must explicitly state the type as Set. If you don’t, Swift will infer an Array.

    // Creates a Set of Strings. Duplicates ("Apple") are automatically removed.
    let uniqueFruits: Set<String> = ["Apple", "Banana", "Apple", "Mango"]
    print(uniqueFruits) // Output might be something like: ["Banana", "Mango", "Apple"] (order not guaranteed)
    
  2. Explicit Type Annotation (Empty Set):

    // An empty Set of Integers
    var primeNumbers: Set<Int> = []
    print("Initial prime numbers set: \(primeNumbers)")
    

Adding and Removing Elements from Sets

Sets are mutable if declared with var.

var favoriteGenres: Set<String> = ["Rock", "Pop", "Jazz"]
print("Initial genres: \(favoriteGenres)")

// 1. Add an element
favoriteGenres.insert("Classical")
print("After adding Classical: \(favoriteGenres)")

// 2. Try to add an existing element (it will be ignored)
favoriteGenres.insert("Pop")
print("After trying to add existing Pop: \(favoriteGenres)") // No change

// 3. Remove an element
if let removedGenre = favoriteGenres.remove("Jazz") {
    print("Removed genre: \(removedGenre)")
}
print("After removing Jazz: \(favoriteGenres)")

// 4. Remove a non-existent element (returns nil)
if favoriteGenres.remove("Country") == nil {
    print("Country was not in the set.")
}

// 5. Check if a set contains an element
print("Does the set contain Rock? \(favoriteGenres.contains("Rock"))")
print("Does the set contain Blues? \(favoriteGenres.contains("Blues"))")

// 6. Remove all elements
favoriteGenres.removeAll()
print("After removing all: \(favoriteGenres)")
print("Is favoriteGenres empty now? \(favoriteGenres.isEmpty)")

Set Operations

Sets are powerful for comparing groups of items.

let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let primeDigits: Set = [2, 3, 5, 7]

// Union: All unique values from both sets
print("Union of odd and even: \(oddDigits.union(evenDigits).sorted())") // sorted() for predictable output

// Intersection: Common values in both sets
print("Intersection of odd and prime: \(oddDigits.intersection(primeDigits).sorted())")

// Subtracting: Values in the first set NOT in the second
print("Odd digits not prime: \(oddDigits.subtracting(primeDigits).sorted())")

// Symmetric Difference: Values unique to each set (not in common)
print("Symmetric difference of odd and prime: \(oddDigits.symmetricDifference(primeDigits).sorted())")

// Subset / Superset
let singleDigitPrimes: Set = [3, 5]
print("Is singleDigitPrimes a subset of oddDigits? \(singleDigitPrimes.isSubset(of: oddDigits))")
print("Is oddDigits a superset of singleDigitPrimes? \(oddDigits.isSuperset(of: singleDigitPrimes))")
print("Are oddDigits and primeDigits disjoint (no common elements)? \(oddDigits.isDisjoint(with: primeDigits))")

Choosing the Right Collection

This is a common question! Here’s a quick guide:

  • Use an Array when:

    • The order of items matters.
    • You need to access items by their numerical index.
    • You might have duplicate items.
    • Example: A list of tasks, a sequence of game moves, a user’s browsing history.
  • Use a Dictionary when:

    • You need to store associations between keys and values.
    • You want to retrieve values quickly using a unique identifier (the key).
    • The order of items doesn’t matter.
    • Example: User profiles (username -> user data), configuration settings (setting name -> value), a lookup table.
  • Use a Set when:

    • You need to ensure all items are unique.
    • The order of items doesn’t matter.
    • You need to perform mathematical set operations (union, intersection, etc.).
    • Example: A collection of unique tags, tracking visited pages, managing permissions.

Step-by-Step Implementation: Building a Simple Contact Manager

Let’s put our knowledge of collections into practice by building a very basic contact manager. We’ll use a combination of arrays and dictionaries to store contact information.

Open up your Xcode playground or a new Swift file.

Step 1: Define a Contact Structure

First, let’s define what a “contact” looks like. We’ll use a struct for this, which we briefly touched upon in Chapter 4. A struct allows us to group related properties together.

// MARK: - Contact Structure
struct Contact {
    let firstName: String
    let lastName: String
    var phoneNumber: String
    var email: String? // Email is optional, a contact might not have one
}

Explanation:

  • We define a struct named Contact.
  • It has firstName and lastName (constants, as names usually don’t change).
  • phoneNumber is a var because it might change.
  • email is an Optional String (String?) because not every contact will have an email address. This is a great use case for optionals!

Step 2: Create an Array to Store Contacts

Now, let’s create an array to hold multiple Contact objects.

// MARK: - Contact Array
var myContacts: [Contact] = []
print("Initial contacts array: \(myContacts)")

Explanation:

  • var myContacts: [Contact] = [] declares a mutable array named myContacts.
  • Its type is [Contact], meaning it can only hold instances of our Contact struct.
  • [] initializes it as an empty array.

Step 3: Add Some Contacts

Let’s add a few contacts to our array.

// MARK: - Add Contacts
let contact1 = Contact(firstName: "Alice", lastName: "Smith", phoneNumber: "555-1234", email: "[email protected]")
let contact2 = Contact(firstName: "Bob", lastName: "Johnson", phoneNumber: "555-5678", email: nil) // Bob doesn't have an email
let contact3 = Contact(firstName: "Charlie", lastName: "Brown", phoneNumber: "555-9012", email: "[email protected]")

myContacts.append(contact1)
myContacts.append(contact2)
myContacts.append(contact3)

print("\nContacts after adding:")
for contact in myContacts {
    print("\(contact.firstName) \(contact.lastName) - Phone: \(contact.phoneNumber), Email: \(contact.email ?? "N/A")")
}

Explanation:

  • We create three Contact instances, providing values for their properties. Notice how contact2 sets email to nil.
  • We use the append() method to add each contact to our myContacts array.
  • Then, we iterate through the array using a for-in loop to print each contact’s details.
  • contact.email ?? "N/A" uses the nil-coalescing operator (from Chapter 6) to display “N/A” if the email is nil.

Step 4: Update a Contact’s Information

Let’s say Alice gets a new phone number.

// MARK: - Update Contact
if let index = myContacts.firstIndex(where: { $0.firstName == "Alice" && $0.lastName == "Smith" }) {
    myContacts[index].phoneNumber = "555-4321" // Update Alice's phone number
    print("\nAlice's updated phone number:")
    print("\(myContacts[index].firstName) \(myContacts[index].lastName) - Phone: \(myContacts[index].phoneNumber)")
}

Explanation:

  • myContacts.firstIndex(where: { ... }) is a powerful method that searches the array for the first element that satisfies a given condition (defined in the curly braces, which is a closure – we’ll learn more about these in a later chapter!). Here, we’re looking for Alice Smith.
  • This method returns an Optional<Int> (the index if found, or nil if not). We use if let to safely unwrap it.
  • If Alice is found, we use her index to access her Contact object in the array and directly modify its phoneNumber property.

Step 5: Remove a Contact

Charlie decides to go off-grid. Let’s remove him.

// MARK: - Remove Contact
if let index = myContacts.firstIndex(where: { $0.firstName == "Charlie" }) {
    let removedContact = myContacts.remove(at: index)
    print("\nRemoved contact: \(removedContact.firstName) \(removedContact.lastName)")
}

print("\nContacts after removing Charlie:")
for contact in myContacts {
    print("\(contact.firstName) \(contact.lastName) - Phone: \(contact.phoneNumber), Email: \(contact.email ?? "N/A")")
}

Explanation:

  • Similar to updating, we first find Charlie’s index.
  • myContacts.remove(at: index) removes the contact at that specific index and returns the removed Contact object.

Step 6: Using a Dictionary for Quick Lookup

What if we want to quickly find a contact by their full name? Iterating through an array every time can be slow for many contacts. A dictionary is perfect for this!

// MARK: - Dictionary for Quick Lookup
var contactsByName: [String: Contact] = [:]

// Populate the dictionary from our array
for contact in myContacts {
    let fullName = "\(contact.firstName) \(contact.lastName)"
    contactsByName[fullName] = contact
}

print("\nContacts by Name Dictionary: \(contactsByName)")

// Now, try to find Bob quickly
let nameToFind = "Bob Johnson"
if let bob = contactsByName[nameToFind] {
    print("\nFound \(bob.firstName) \(bob.lastName) via dictionary lookup. Phone: \(bob.phoneNumber)")
} else {
    print("\n\(nameToFind) not found in dictionary.")
}

Explanation:

  • We create an empty dictionary contactsByName where keys are String (full name) and values are Contact objects.
  • We loop through our myContacts array. For each contact, we create a fullName string and use it as the key to store the Contact object in contactsByName.
  • Now, finding a contact by full name is a single dictionary lookup, which is very efficient!

Mini-Challenge: Holiday Wish List

You’re tasked with creating a holiday wish list application.

Challenge:

  1. Create a Set called myWishList to store unique String items.
  2. Add a few items to your wish list (e.g., “New Headphones”, “Coffee Maker”, “Book”).
  3. Add another item that’s already on the list (e.g., “New Headphones”) and observe what happens.
  4. Create a second Set called friendsWishList with a few items, including some that overlap with myWishList (e.g., “Coffee Maker”, “Gaming Console”, “Book”).
  5. Find out which items are on both wish lists (the intersection).
  6. Find out all unique items across both wish lists (the union).

Hint: Remember that Sets automatically handle uniqueness. Use the insert() method to add items and the intersection() and union() methods for comparing sets.

Click for Solution (but try it yourself first!)
// 1. Create a Set for your wish list
var myWishList: Set<String> = []
print("My initial wish list: \(myWishList)")

// 2. Add items
myWishList.insert("New Headphones")
myWishList.insert("Coffee Maker")
myWishList.insert("Book")
print("My wish list after adding items: \(myWishList)")

// 3. Add an item already on the list
myWishList.insert("New Headphones") // This will be ignored
print("My wish list after trying to add duplicate: \(myWishList)") // No change!

// 4. Create a friend's wish list
let friendsWishList: Set<String> = ["Coffee Maker", "Gaming Console", "Book", "Smartwatch"]
print("Friend's wish list: \(friendsWishList)")

// 5. Find common items (intersection)
let commonItems = myWishList.intersection(friendsWishList)
print("Items on both wish lists: \(commonItems.sorted())") // Using .sorted() for predictable output

// 6. Find all unique items across both lists (union)
let allUniqueItems = myWishList.union(friendsWishList)
print("All unique items across both lists: \(allUniqueItems.sorted())")

Common Pitfalls & Troubleshooting

Working with collections is powerful, but there are a few common traps beginners often fall into:

  1. Array Index Out of Bounds:

    • Pitfall: Trying to access an element at an index that doesn’t exist (e.g., myArray[10] when myArray only has 5 elements). This will cause a runtime crash.
    • Solution: Always check the array’s count property before accessing an index, or use methods like first and last which return Optionals for safe access. When iterating with a for-in loop, you don’t need to worry about this.
    let numbers = [10, 20, 30]
    // print(numbers[3]) // ❌ CRASH! Index out of bounds
    
    if numbers.count > 2 {
        print(numbers[2]) // ✅ Safe access
    }
    
    if let thirdNumber = numbers.dropFirst(2).first { // Another safe way to get the 3rd element
        print(thirdNumber)
    }
    
  2. Forgetting Optional Unwrapping for Dictionary/Set Access:

    • Pitfall: When accessing a value from a Dictionary using its key, or removing an element from a Set, the result is an Optional. Forgetting to unwrap it (or force-unwrapping when nil is possible) can lead to unexpected nil values or crashes.
    • Solution: Always use if let, guard let, or the nil-coalescing operator (??) when dealing with optional return values from collection access.
    let inventory = ["Laptop": 5, "Mouse": 10]
    let quantity = inventory["Keyboard"] // Type is Int?
    
    // print("Keyboard quantity: \(quantity + 1)") // ❌ ERROR: Cannot add Int? and Int
    
    if let keyboardQuantity = inventory["Keyboard"] {
        print("Keyboard quantity: \(keyboardQuantity)")
    } else {
        print("Keyboard not found in inventory.")
    }
    
    let defaultQuantity = inventory["Monitor"] ?? 0
    print("Monitor quantity (defaulting to 0): \(defaultQuantity)")
    
  3. Mutable vs. Immutable Collections (let vs. var):

    • Pitfall: Declaring a collection with let makes it immutable, meaning you cannot add, remove, or modify its elements. Trying to do so will result in a compile-time error.
    • Solution: If you intend to change the contents of a collection, declare it with var. If the collection should never change after creation, use let for safety and clarity.
    let immutableArray = ["A", "B"]
    // immutableArray.append("C") // ❌ ERROR: Cannot use mutating member on immutable value
    
    var mutableArray = ["X", "Y"]
    mutableArray.append("Z") // ✅ Works
    print(mutableArray)
    
  4. Type Mismatch in Collections:

    • Pitfall: Trying to add an element of a different type to a type-safe collection.
    • Solution: Swift’s type inference and explicit type annotations will usually catch this at compile time. Ensure all elements you add match the collection’s declared type.
    var numbers: [Int] = [1, 2, 3]
    // numbers.append("Four") // ❌ ERROR: Cannot convert value of type 'String' to expected element type 'Int'
    

Summary

Phew! We’ve covered a lot in this chapter, and you’ve taken a massive leap in your ability to manage data in Swift. Here are the key takeaways:

  • Collections are fundamental structures for storing multiple values.
  • Swift’s standard collections (Arrays, Dictionaries, Sets) are value types and type-safe.
  • Arrays are ordered collections that allow duplicates, accessed by zero-based indices. Use them when order and index-based access are important.
  • Dictionaries are unordered collections of unique key-value pairs. Use them for fast lookups based on a specific key.
  • Sets are unordered collections of unique values. Use them when uniqueness and efficient set operations are crucial.
  • Always be mindful of optional unwrapping when accessing dictionary values or removing set elements.
  • Use var for mutable collections and let for immutable ones.
  • Be careful to avoid array index out of bounds errors.

Understanding and effectively using these collection types is a cornerstone of writing robust and efficient Swift applications. You’re now equipped to handle more complex data structures!

In our next chapter, we’ll explore memory management in Swift, understanding how your app handles memory for objects, which is crucial for building performant and stable applications.


References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.