Swift Sets

by Dimitris Tasios
7 views

In this article, we’ll take a look into one of Swift’s collection types, Swift Sets. We’ll see how to create a Set, some of its characteristics, and a few main functions, through examples.

If you would like to read about Swift’s other two collection types, you may read about Arrays in this article, and Dictionaries in this article.

1. What Are Sets in Swift?

In Swift, Sets are collection types, which means that they represent a collection of multiple different values of the same type. They contain unique values, meaning that each element is found only once. Also, they don’t have a particular order when iterating through them (actually there is a way to “sort” them, but more on that later).

The uniqueness of an item depends on its hash. A hash is an Int value that each element of a Set has and computes for itself. This value provides a way for the element to identify itself uniquely within the Set. If two elements have the same hash, they are considered the same by the Set. Basic types (Int, Double, String, Bool) compute their hash themselves. Custom data types, on the other hand, must provide computation for this hash, by implementing the Hashable protocol.

In the following image, you can get an idea of how a Set would look. This Set represents a set of colors:

Figure 1. A Set of colors.

As you can see, items are unordered, unlike Arrays. Furthermore, if we tried to add one more “Fuchsia” element, the number of elements in the Set would remain the same. That is because Sets in Swift cannot have duplicate values.

2. How to Create Sets in Swift?

In Swift, creating a Set requires us to use the Set<ElementType> syntax; there’s no shorthand syntax like Arrays.

2.1 Creating an Empty Set

In order to create an empty Set of type String, we would write the following:

var emptyStringSet: Set<String> = [] // a mutable empty Set of Strings
var anotherEmptyStringSet = Set<String>() // another way to write it, with type inference

print(emptyStringSet.isEmpty)
print(anotherEmptyStringSet.isEmpty)

The output is:

true
true

In the example above, we’ve seen two ways of creating an empty Set.

  • The first one (line 1) explicitly declares the Set’s type using the Set<ElementType> syntax and then assigns a value of [] to it.
  • The second one (line 2) uses type inference to “understand” its type, based on the value after the equals sign (=). In this case, we use the Set<ElementType> syntax, followed by parenthesis.

The isEmpty property is available to all Swift Sets and informs us about whether the Set is empty or not. As you can see, both of those Sets are empty. The way we declare an empty Set comes down to preference.

2.2 Creating a Non-empty Set

On the other hand, we can create a non-empty Set at once, instead of creating an empty one and filling it later. In order to create a non-empty Set, we enclose all values we want it to include in square brackets ([]), just like Arrays. However, we have to explicitly state that we want a Set, otherwise we would get an Array.

Let’s see all these in action, for more clarity:

var colorsSet: Set<String> = ["Red", "Green", "Yellow", "Fuchsia", "Black", "White"]    // Set
var colorsSet2: Set = ["Red", "Green", "Yellow", "Fuchsia", "Black", "White"]           // Set, written in a briefer way
var colorsArray = ["Red", "Green", "Yellow", "Fuchsia", "Black", "White"]               // Array, since we didn't explicitly state it as a Set

print("The type of \"colorsSet\" is: \(type(of: colorsSet))")
print("The type of \"colorsSet2\" is: \(type(of: colorsSet2))")
print("The type of \"colorsArray\" is: \(type(of: colorsArray))")

The output is:

The type of "colorsSet" is: Set<String>
The type of "colorsSet2" is: Set<String>
The type of "colorsArray" is: Array<String>

As you can see, if we want to create a non-empty Set, we have to explicitly define it a Set. In line 1, we define both the type of structure (Set) and its type (String). On the contrary, in line 2 the type of Set is known through type inference, so we can omit it entirely. However, we can’t omit the type of structure, because a syntax like the one in line 3 will create an Array instead. This is shown by printing the type of all those variables; the colorsArray is an Array, while the other two variables are Sets.

The function type(of:) returns the type of the passed variable.

Lastly, as we have mentioned, once the type of a Set is declared, all of its values must be of the same time. If not, then we’ll get a compilation error:

var invalidSet: Set = [1, 55, 5.7, ""] // Invalid, all elements must be of the same type

However, if the values can all be cast into a type, we can have something like the following:

var validSet: Set = [4, 66.4]               // 4 is converted into Double
var alsoValidSet: Set<Double> = [4, 66.4]   // same as above

print(validSet)
print(alsoValidSet)

The output is:

[4.0, 66.4]
[4.0, 66.4]

In this case, the integer 4 is converted into a Double of value 4.0 and the entire Set is of type Double.

If we defined that Set as an Int, we would get a compilation error:

var invalidNumberSet: Set<Int> = [4, 66.4] // Invalid, 66.4 is not an Int

3. Operations and Functions of Swift Sets

In this section, we’ll see some main operations and a few functions that we can perform on Sets.

3.1 Checking the Number of Elements of a Set With the count Property

With this property, we can get the number of elements in a Set. If the Set is empty, then 0 is returned.

var europeanLanguages: Set = ["English", "Spanish", "Italian", "Greek"]
var noLanguages = Set<String>()

print("Number of languages in \"europeanLanguages\": \(europeanLanguages.count)")
print("Number of languages in \"noLanguages\": \(noLanguages.count)")

The output is:

Number of languages in "europeanLanguages": 4
Number of languages in "noLanguages": 0

3.2 Checking if a Set Is Empty With the isEmpty Property

This property returns true if the Set contains no elements. Otherwise, it returns false.

var europeanLanguages: Set = ["English", "Spanish", "Italian", "Greek"]
var noLanguages = Set<String>()

print("Is \"europeanLanguages\" empty: \(europeanLanguages.isEmpty)")
print("Is \"noLanguages\" empty: \(noLanguages.isEmpty)")

The output is:

Is "europeanLanguages" empty: false
Is "noLanguages" empty: true

3.3 Inserting an Element Into a Set

We can add new elements in a Set, with the insert() function. Modifying the number of elements of a Set can only be done when the Set is mutable (see section 4 below). Since there is no order of elements in a Set, we can’t know where the Set will insert the new element. The element will only be inserted if the Set doesn’t already have that element.

var europeanLanguages: Set = ["English", "Spanish", "Italian", "Greek"]

europeanLanguages.insert("French")
print("Number of languages in \"europeanLanguages\" after inserting \"French\": \(europeanLanguages.count)")

// inserting an element that already exists in the Set
europeanLanguages.insert("Spanish")
print("Number of languages in \"europeanLanguages\"after inserting \"Spanish\" again: \(europeanLanguages.count)")

The output is:

Number of languages in "europeanLanguages" after inserting "French": 5
Number of languages in "europeanLanguages"after inserting "Spanish" again: 5

3.4 Sorting the Elements of a Set

So far, we have mentioned that the elements of a Set don’t have a specific order. However, if the elements conform to the Comparable protocol, we can an Array with all of the Set’s values sorted. By default, all basic data types of Swift are comparable.

var europeanLanguages: Set = ["English", "Spanish", "Italian", "Greek"]

let europeanLanguagesSorted = europeanLanguages.sorted() // Array of Strings with the values of Set sorted
print(europeanLanguagesSorted)
print(europeanLanguages) // with each run, the elements will be printed in a different order

The output is:

["English", "Greek", "Italian", "Spanish"]
["Spanish", "Italian", "English", "Greek"]

Note that the print statement of line 3 might have a different output for you. That’s because the elements of a Set are printed in whichever order Swift finds them. On the other hand, the elements of line 2 are part of an Array, so their order is always the same.

3.5 Removing an Element From a Set

In this subsection, we’ll see functions that remove elements from a Set. Modifying the number of elements of a Set can only be done when the Set is mutable (see section 4 below). Removing an element from an empty Set will return nil.

3.5.1 Removing a Specific Element With remove(_ member:)

With this function, we provide the element we would like to remove from the Set. It, then, returns the removed element. If the element that we want to remove doesn’t exist in the Set, this function will return nil.

var europeanLanguages: Set = ["English", "Spanish", "Italian", "Greek"]

var greek = europeanLanguages.remove("Greek") // can be nil
print("Removed element: \(greek ?? "\"Greek not found\"")")

print("European languages Set after removing \"Greek\": \(europeanLanguages)\n")

var lithuanian = europeanLanguages.remove("Lithuanian") // can be nil
print("Removed element: \(lithuanian ?? "\"Lithuanian not found\"")")

print("European languages Set after removing \"Lithuanian\": \(europeanLanguages)\n")

The output is:

European languages Set initially: ["English", "Spanish", "Italian", "Greek"]

Removed element: Greek
European languages Set after removing "Greek": ["English", "Spanish", "Italian"]

Removed element: "Lithuanian not found"
European languages Set after removing "Lithuanian": ["English", "Spanish", "Italian"]

3.5.2 Removing the First Element of a Set With removeFirst()

This function removes the first element of the Set. Due to the fact that Sets have no order, the element that will be removed is not known beforehand.

To demonstrate this, we’ll use this function 100 times, while resetting the Set in each iteration, with the help of a for loop.

var europeanLanguages: Set = ["English", "Spanish", "Italian", "Greek"]

var englishRemovedTimes = 0
var spanishRemovedTimes = 0
var italianRemovedTimes = 0
var greekRemovedTimes = 0

for _ in 0...99 {
    europeanLanguages = ["English", "Spanish", "Italian", "Greek"] // resetting Set
    let language = europeanLanguages.removeFirst()
    if language == "English" {
        englishRemovedTimes += 1
    } else if language == "Spanish" {
        spanishRemovedTimes += 1
    } else if language == "Italian" {
        italianRemovedTimes += 1
    } else {
        greekRemovedTimes += 1
    }
}

print("English removed \(englishRemovedTimes) times")
print("Spanish removed \(spanishRemovedTimes) times")
print("Italian removed \(italianRemovedTimes) times")
print("Greek removed \(greekRemovedTimes) times")

The output is:

English removed 30 times
Spanish removed 21 times
Italian removed 36 times
Greek removed 13 times

As you can see, the Set removes a random element in every iteration. Those numbers change every time we run the for-loop, so you might get a different output.

3.5.3 Removing an Element at a Specific Index of a Set With remove(at:)

With this function, we can remove an element from a specific index of a Set. On its own, this function will have random results, just like removeFirst().

However, we can combine this function with the firstIndex(of:) function, which returns the index of an element. The combination of those two functions will do the same thing as the
remove(_ member:) function of subsection 3.5.1.

var europeanLanguages = ["English", "Spanish", "Italian", "Greek"]

if let indexOfEnglish = europeanLanguages.firstIndex(of: "English") {
    let englishRemoved = europeanLanguages.remove(at: indexOfEnglish)
    
    print("Removed: \(englishRemoved)")
    print(europeanLanguages)
}

The output is:

Removed: English
["Spanish", "Greek", "Italian"]

In this example, we had to make sure that the optional indexOfEnglish wasn’t nil, so that it could be used for englishRemoved. If you would like to read more about optionals, you can check out our article here.

Of course, we don’t have to use firstIndex(of:), but we could use any valid index instead. A valid index is an index that is both non-negative and not greater than Set.count - 1. Trying to remove an element at an invalid index, will cause a run-time error.

var noLanguages = Set<String>()
let invalidRemoval = noLanguages.remove(at: noLanguages.startIndex) // error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).

The output is:

Swift/NativeSet.swift:255: Fatal error: Attempting to access Set elements using an invalid index

3.5.4 Removing all Elements of a Set With removeAll()

Lastly, with this function, we can remove all elements of a Set.

var europeanLanguages = ["English", "Spanish", "Italian", "Greek"]

print("European language Set before removing all elements: \(europeanLanguages)\nIs it empty: \(europeanLanguages.isEmpty)")
europeanLanguages.removeAll()
print("\nEuropean language Set after removing all elements: \(europeanLanguages)\nIs it empty: \(europeanLanguages.isEmpty)")

The output is:

European language Set before removing all elements: ["Italian", "Spanish", "English", "Greek"]
Is it empty: false

European language Set after removing all elements: []
Is it empty: true

3.6 Checking if a Set Contains an Element

The last function we’ll take a look into is contains(_ :). This function checks whether the Set contains an element, returning true if it does and false otherwise.

var europeanLanguages = ["English", "Spanish", "Italian", "Greek"]

print("European languages Set contains \"Italian\": \(europeanLanguages.contains("Italian"))")
print("European languages Set contains \"German\": \(europeanLanguages.contains("German"))")

The output is:

European languages Set contains "Italian": true
European languages Set contains "German": false

4. Mutability of Swift Sets

As you might have noticed, in all of our examples we have declared Sets as variables with the var keyword. This makes those Sets mutable. In a mutable Set, we can:

  1. Add more elements.
  2. Remove elements from it.

If we create a Set as a constant with the let keyword, then the Set becomes immutable. In an immutable Set, we cannot perform the aforementioned two operations, because such a Set doesn’t allow changes. If we try to perform those operations, we’ll get a compile-time error.

let constantSet: Set = [1, 4, 3]
constantSet.insert(2) // Cannot use mutating member on immutable value: 'constantSet' is a 'let' constant
constantSet.remove(1) // Cannot use mutating member on immutable value: 'constantSet' is a 'let' constant

You may read more about variables and constants in Swift in our article here.

5. Iterating through Swift Sets With for Loops

We can use for loops with Sets in order to access all of their elements one by one. Since the elements of a Set don’t have a specific order, we can’t know the order that the for-loop we iterate through the elements of the Set. As such, using the sorted() function (or converting our Set into an Array) we can get an Array whose elements have a predictable order.

let numbersSet: Set = [-4, 3, 10, 6, 2, 22, -100]
for number in numbersSet {
    print(number)
}

print()

let numbersArray = Array(numbersSet) // for a sorted array use numberSet.sorted()
for number in numbersArray {
    print(number)
}

The output is:

-4
-100
3
10
22
6
2

-4
-100
3
10
22
6
2

In line 7, we convert our Set into an Array. Running the code would have a different output for you. As you can see, the order of the elements of the Set when we declared it (line 1) is different than the one when we print its elements.

6. Creating a New Set Based on Values of Two Other Sets

So far, we have focused on the operations we can perform on a single Set, like inserting elements or checking if it’s empty. In this section, we’ll focus on the creation of a new Set, based on two other Sets. This will be done thanks to some fundamental mathematical operations, which we can also use in Swift.

Those operations are the following:

  1. Intersection
  2. Symmetric Difference
  3. Union
  4. Subtraction

They will be analyzed in the following subsections. We’ll be using the names of the Swift functions directly, instead of the names we used above. The values of the new Set are the ones within the orange area. We will also mention if each of these four operations yields the symmetric property.

The symmetric property states that the order of the operands in the operation doesn’t affect the result of the operation. For example, addition is an operation that has the symmetric property, since 4+3 has the same result as 3+4.

6.1 intersection()

The first operation is intersection(). It creates a Set based on the values that two Sets have in common. The intersection is an operation that has the symmetric property, so the order of operands in the operation doesn’t affect the final result.

Figure 2. The intersection (orange area) of two Sets A and B.

Here’s an example of using this function in action:

let oddNumbersSet: Set = [1, 3, 5, 7, 9]            // odd numbers
let sequentialNumbersSet: Set = [1, 2, 3, 4, 5]     // sequential numbers

let intersectionSet = oddNumbersSet.intersection(sequentialNumbersSet)           // 1, 3, 5 (in any order)
let oppositeIntersectionSet = sequentialNumbersSet.intersection(oddNumbersSet)   // 1, 3, 5 (in any order)

Since the elements of Sets are unordered, printing the elements of intersectionSet and oppositeIntersectionSet might yield a different order between the two. Our main focus is on which elements exist within the new Set, not their order.

6.2 symmetricDifference()

Contrary to the Intersection, the symmetricDifference() operation creates a Set based on the values that exist in either Sets but, not in both of them. This operation also has the symmetric property.

Figure 3. The symmetric difference (orange area) of two Sets A and B.
let oddNumbersSet: Set = [1, 3, 5, 7, 9]            // odd numbers
let sequentialNumbersSet: Set = [1, 2, 3, 4, 5]     // sequential numbers

let symmetricDifferenceSet = oddNumbersSet.symmetricDifference(sequentialNumbersSet)            // 2, 4, 7, 9 (in any order)
let oppositeSymmetricDifferenceSet = sequentialNumbersSet.symmetricDifference(oddNumbersSet)   // 2, 4, 7, 9 (in any order)

6.3 union()

The union() operation creates a Set based on all values of both Sets. This operation also has the symmetric property.

Figure 4. The union (orange area) of two Sets A and B.
let oddNumbersSet: Set = [1, 3, 5, 7, 9]            // odd numbers
let sequentialNumbersSet: Set = [1, 2, 3, 4, 5]     // sequential numbers

let unionSet = oddNumbersSet.union(sequentialNumbersSet)            // 1, 2, 3, 4, 5, 7, 9 (in any order)
let oppositeUnionSet = sequentialNumbersSet.union(oddNumbersSet)    // 1, 2, 3, 4, 5, 7, 9 (in any order)

6.4 subtracting()

Finally, the last operation we’ll see is subtracting(). It creates a Set based on all values that exist in the first Set but not the second. In a way, you can think of it like this: you take the first operand (the first Set) and remove all the elements that also exist in the second Set. That’s what this operation does.

Because of that, this operation doesn’t have the symmetric property. By removing the common values of each Set, it’s not guaranteed that both Sets will have the same remaining elements.

Graphically, this operation looks like this:

Figure 5. The subtraction (orange area) of Set B from Set A.
let oddNumbersSet: Set = [1, 3, 5, 7, 9]            // odd numbers
let sequentialNumbersSet: Set = [1, 2, 3, 4, 5]     // sequential numbers

let commonNumbersSet = oddNumbersSet.intersection(sequentialNumbersSet)                     // 1, 3, 5
let subtractingOddSetFromSequentialSet = sequentialNumbersSet.subtracting(oddNumbersSet)    // 2, 4 (in any order)
let subtractingSequentialSetFromOddSet = oddNumbersSet.subtracting(sequentialNumbersSet)    // 7, 9 (in any order)

In the example above, commonNumbersSet contains the common numbers (intersection) of those two Sets. The subtractingOddSetFromSequentialSet in line 5 contains only the elements that don’t exist in subtractingSequentialSetFromOddSet in line 6 and vice versa.

7. Comparing the Set Membership Between Two Sets

Lastly, in this section, we’ll see a few functions that help us understand the relation of two Sets, by comparing both how many values they have and, also, which of them exist in those Sets.

7.1 Checking if Sets Are Equal With the == Operator

The simplest way to compare two Sets is to check if they have the same values. This is simply done with the == operator. This operation, apart from the symmetric property, has another kind of property, the reflexive property.

The reflexive property states that the operand is equal to itself. For example, the equals sign (=) has the reflexive property, since 5=5.

let numbersSetA: Set = [-1, 44, 10]
let numbersSetB: Set = [10, -1, 44]

let areSetsEqual = (numbersSetA == numbersSetB)         // true, order doesn't matter
let hasSymmetricProperty = (numbersSetB == numbersSetA) // true, symmetric property exists
let hasReflexiveProperty = (numbersSetA == numbersSetA) // true, a Set is equal to itself, known as the "reflexive" property

7.2 Checking if One Set Is a Subset or a Superset of Another Set

We’ll see two functions in this section that return a Bool.

The first one is isSubset(of:). This function checks whether one Set is a subset of another Set. For example, if a set A is a subset of a set B, this means that all of A‘s elements exist in B.

Similarly, the second function, isSuperset(of:) checks whether one Set is a superset of another Set. For example, if a set B is a superset of a set A, this means that all of B contains all of A‘s elements. These functions are basically the two sides of the same coin. Both Sets are allowed to be equal to each other, but it’s not compulsory.

Both of those operations have reflexive properties. However, they only have symmetric properties, if both Sets are equal.

Let’s take a look at how this would look graphically:

Figure 6. Three Sets: A, B, and C.

In the figure above, we have 3 sets: A (in purple), B (in petrol), and C (in yellow). A is a subset of B (the opposite is not true). Similarly, B is a superset of A (the opposite is not true). The opposites aren’t true, because B has all of A‘s elements but a few more. We will see this kind of relationship in the next subsection.

On the other hand, C is both a superset and a subset of B and vice versa. This is true because B and C have the exact same elements (indicated by the new green color, created from the combination of B and C).

let setA: Set = [1, 2, 3, 4, 5]
let setB: Set = [1, 2, 3 ,4 ,5, 6]
let setC: Set = [1, 2, 3 ,4 ,5, 6]

let aSubseta = setA.isSubset(of: setA)      // true
let aSubsetB = setA.isSubset(of: setB)      // true
let bSubsetA = setB.isSubset(of: setA)      // false

let bSupersetB = setB.isSuperset(of: setB)  // true
let bSupersetA = setB.isSuperset(of: setA)  // true
let aSupersetB = setA.isSuperset(of: setB)  // false

let cSubsetB = setC.isSubset(of: setB)      // true
let cSupersetB = setC.isSuperset(of: setB)  // true
let bSubsetC = setB.isSubset(of: setC)      // true
let bSupersetC = setB.isSuperset(of: setC)  // true

7.3 Checking if One Set Is a Proper (Strict) Subset or a Proper (Strict) Superset of Another Set

In the section above, we saw that a subset and a superset of two Sets can mean that the Sets can either be equal to each other or not. In this section, we’ll see two similar functions that check whether one Set is a proper (also known as strict) subset or proper (strict) superset of another Set. The difference with being a proper/strict Set is that the Sets aren’t equal (so the strict operations don’t have the symmetric property). We’ll use the isStrictSubset(of:) and isStrictSuperset(of:) functions.

Let’s take a look at Figure 6 again and replace all isSubset(of:) and isSuperset(of:) functions with their “strict” versions.

Figure 7. The same three Sets: A, B, and C, from Figure 6, for better understanding.
let setA: Set = [1, 2, 3, 4, 5]
let setB: Set = [1, 2, 3 ,4 ,5, 6]
let setC: Set = [1, 2, 3 ,4 ,5, 6]

let aStrictSubseta = setA.isStrictSubset(of: setA)      // false
let aStrictSubsetB = setA.isStrictSubset(of: setB)      // true
let bStrictSubsetA = setB.isStrictSubset(of: setA)      // false

let bStrictSupersetB = setB.isStrictSuperset(of: setB)  // false
let bStrictSupersetA = setB.isStrictSuperset(of: setA)  // true
let aStrictSupersetB = setA.isStrictSuperset(of: setB)  // false

let cStrictSubsetB = setC.isStrictSubset(of: setB)      // false
let cStrictSupersetB = setC.isStrictSuperset(of: setB)  // false
let bStrictSubsetC = setB.isStrictSubset(of: setC)      // false
let bStrictSupersetC = setB.isStrictSuperset(of: setC)  // false

As you can see, we have a few differences now that we use the stricter definition of superset and subset. In all of the marked lines in the snippet above, we can see that a Set is neither a proper/strict subset nor a proper/strict subset of itself (not a reflexive property) or other Sets that are equal to it. We can also see that a proper/strict subset or proper/strict superset is also a subset or superset respectively, but the opposite is not true.

7.4 Checking if Two Sets Have Any Common Elements

The last function we’ll see is isDisjoint(with:). This function checks whether two Sets are disjoint, which means that they have no elements in common.

This operation has the symmetric property but, not the reflexive property, since a Set can’t be disjoint with itself.

Figure 8. Two disjoint Sets: A and B.
let negativeNumbersSet: Set = [-3, -2, -1]
let positiveNumbersSet: Set = [1, 2, 3]

let positiveDisjointPositive = positiveNumbersSet.isDisjoint(with: positiveNumbersSet) // false
let positiveDisjointNegative = positiveNumbersSet.isDisjoint(with: negativeNumbersSet) // true
let negativeDisjointPositive = negativeNumbersSet.isDisjoint(with: positiveNumbersSet) // true
let negativeDisjointNegative = negativeNumbersSet.isDisjoint(with: negativeNumbersSet) // false

As you can see, in lines 5-6 we can see that the symmetric property holds true, while the same cannot be said for the reflexive property in lines 4 and 7.

8. Conclusion

By now, you should have a clear understanding of how to create and use Sets in Swift. You can find the source code (Playground) on our GitHub page.

9. Sources

[1] Hashable Protocol – Apple

[2] Comparable Protocol – Apple

Related Posts