WWDC Quick Look 💓 By SwiftGGTeam
Meet the Swift Algorithms and Collections packages

Meet the Swift Algorithms and Collections packages

Watch original video

Highlight

Apple released two open source packages, Swift Algorithms and Swift Collections, allowing developers to use new sequence algorithms, deques, ordered sets, and ordered dictionaries now, and using these APIs as an incubator for future standard library capabilities.

Core Content

A chat app often has this kind of code: the user selects a few lines of messages, and you want to get the correspondingMessage; When the user clicks on the picture, you need to collect attachments from the message; the details page should display the last 6 photos. The most direct way to write it isforLoops, temporary arrays,if letbreak

These loops work, but they hide their intent within the control flow. A person reading the code would need to read the entire loop to know whether it is converting, filtering, flattening, or just taking the first few elements. Loops also tend to miss performance details such as capacity reservation and delayed calculations.

The Swift standard library already hasmapcompactMapflatMapprefixsuffixreversedthese algorithms. Swift Algorithms continue to complement patterns not yet covered by the standard library, such as sliding windows, adjacent elements, chunking, and grouping by conditions.

Apple also releases Swift Collections. The Swift standard library has long had only three general categories of data structures:ArraySetDictionary. They are suitable for general boundary types, but scenarios such as queues, ordered unique lists, and ordered key-value tables require data structures that are closer to the problem.

Powered by Swift CollectionsDequeOrderedSetOrderedDictionary. They maintain the value semantics and copy-on-write behavior of Swift collections, while filling in the missing operation model of the standard library.

From primitive loops to algorithmic vocabulary

In the code of the chat app, many loops actually have names. Select line transfer message, yesmap. Collect non-empty attachments, yescompactMap. A message generates multiple transcript items and then flattens them, yesflatMap

With these names, the focus of the code changes from “how to loop” to “what is this step doing”. This is also the positioning of Swift Algorithms: putting missing common patterns into open source packages first, developers now use them, and the community then works together to verify whether they are suitable for inclusion in the standard library.

From general collections to appropriate data structures

ArrayGreat for accessing by subscript, but inserting an element at the beginning requires moving existing elements. Scenarios such as queues, task scheduling, and chat draft undo stacks often insert and remove from both ends.ArrayThe cost is on the high side.

SetUniqueness is guaranteed, but order is not guaranteed. To-do lists, tag lists, and favorites manually sorted by users need to satisfy both “unique elements” and “stable order”. this isOrderedSetlocation.

DictionaryYou can quickly look up values ​​by key, but the order of key-value pairs is not exposed as the core semantics. When fields, parameters, and configuration items need to be displayed in the order of insertion,OrderedDictionaryPut key lookup and order saving in the same type.

Detailed Content

Standard library algorithm: rewrite loops into readable operations

01:00mapUsed to convert each element in the input collection into another value. The example in the lecture comes from the selected row of the table:indexPathsForSelectedRowsWhat is stored in it is the index path, and the App needs to get the corresponding message.

// Raw loop:
var selectedMessages: [Message] = []
for indexPath in indexPathsForSelectedRows {
    selectedMessages.append(messages[indexPath.row])
}

// Using `map` makes this clearer and faster.
indexPathsForSelectedRows.map { messages[$0.row] }

Key points:

  • selectedMessagesis the temporary array in the original loop. -for indexPath in indexPathsForSelectedRowsAccess the selected rows one by one. -messages[indexPath.row]Get the corresponding one according to the line numberMessage
  • mapDirectly express “convert each index path into a message”.
  • The speech stated,mapCapacity will be reserved to avoid intermediate allocation caused by array expansion in the original loop.

01:36compactMapHandles the common pattern of “filter nil and unpack”. In chat apps, messages may or may not have attachments.

// Raw loop:
var attachments: [Attachment] = []
for message in messages {
    if let attachment = message.attachment {
        attachments.append(attachment)
    }
}

// The above is just a `map` and a `filter`.
messages
    .filter { $0.attachment != nil }
    .map { $0.attachment! }

// This pattern is so common we have a special name and algorithm for it.
messages.compactMap { $0.attachment }

Key points:

  • attachmentsCollect all attachments that exist. -if let attachment = message.attachmentOnly non-empty attachments are processed. -filterFirst filter out messages with empty attachments. -mapThen forcefully unpack the optional accessories. -compactMapCombine filtering and unpacking in one step to avoid handwriting!

02:06flatMapHandle “one-to-many conversion”. A message may generate multiple transcript items. If firstmap, the result will become an array within an array.

extension Message {
    func makeMessageParts() -> [TranscriptItem]
}

messages // [Message]
    .map { $0.makeMessageParts() } // [[TranscriptItem]]
    .joined() // [TranscriptItem]

// This pattern is so common that we have another special kind of map for it.
messages // [Message]
    .flatMap { $0.makeMessageParts() }  // [TranscriptItem]

Key points:

  • makeMessageParts()put oneMessageSplit into multipleTranscriptItem
  • mapThe result is[[TranscriptItem]], each message corresponds to an internal array. -joined()Concatenate inner arrays into a flat set. -flatMapDirect expression “flatten after conversion”.

Lazy adapter: Let the algorithm chain calculate on demand

(03:00) The details page only requires the latest 6 photos. The original loop will traverse in reverse, determine the type, count, and exit after reaching 6. Algorithm chains can break down these steps.

// Raw loop:
var photos: [PhotoItem] = []
for item in transcript.reversed() {
    if let photo = item as? PhotoItem {
        photos.append(photo)
        if photos.count == 6 {
            break
        }
    }
}

// The above can be expressed more concisely by chaining together algorithms.
transcript
    .reversed() // [TranscriptItem]
    .compactMap { $0 as? PhotoItem } // [PhotoItem]
    .prefix(6) // [PhotoItem]

// This gives us more flexibility to express this code more clearly.
transcript
    .compactMap { $0 as? PhotoItem } // [PhotoItem]
    .suffix(6) // [PhotoItem]
    .reversed() // [PhotoItem]

Key points:

  • transcript.reversed()Traverse from new to old. -compactMap { $0 as? PhotoItem }Only photo items are retained and type conversion is completed. -prefix(6)Take the first 6 photo items.
  • Another way to write is to take all the photos firstsuffix(6),Againreversed(), closer to the idea of ​​“displaying the last 6 photos in reverse order”.

(04:19) An algorithm chain does not necessarily create an array at each step.joined()returnFlattenSequence, this is a lazy adapter (lazy adapter), the creation cost is very low, and elements are processed on demand.

extension Message {
    func makeMessageParts() -> [TranscriptItem]
}

messages
    .map { $0.makeMessageParts() } // [[TranscriptItem]]
    .joined() // FlattenSequence<[[TranscriptItem]]>

Key points:

  • mapGenerate the item array corresponding to each message. -joined()returnFlattenSequence
  • FlattenSequenceWraps the underlying collection, flattening elements based on access requirements.
  • The talk calls this type of wrapper a lazy adapter.

(04:58) Addition to algorithms with closures.lazy, which allowsmapfiltercompactMapSuch operations are performed lazily.

transcript
    .lazy // LazySequence<[TranscriptItem]>
    .compactMap { $0 as? PhotoItem } // LazyCompactMap<[TranscriptItem], PhotoItem>
    .suffix(6) // LazyCompactMap<ArraySlice<TranscriptItem>, PhotoItem>
    .reversed() // ReversedCollection<LazyCompactMap<ArraySlice<TranscriptItem>, PhotoItem>>

Key points:

  • .lazySwitch subsequent algorithm chains to on-demand computing mode. -compactMapPhoto array is no longer generated immediately. -suffix(6)Only care about the last 6 results. -reversed()Return to reverse view.

(05:48) If you really need an array in the end, useArray(...)Collect the results. The speech emphasized that for transcripts that will be traversed repeatedly, it is more appropriate to finally save them as an array to avoid recalculation every time you enter the edit mode, click on the image, or open the details page.

Array(
    transcript
        .lazy
        .compactMap { $0 as? PhotoItem }
        .suffix(6)
        .reversed()
)

Key points:

  • Array(Start materializing lazy results into arrays. -transcript.lazyLet the intermediate steps execute as needed. -compactMapFilter and transform photo items. -suffix(6)Take the last 6 photos. -reversed()Adjust the display order.

Swift Algorithms: windows, adjacent elements, and tiles

(07:13) Provided by Swift Algorithmswindows(ofCount:). It returns sliding windows. Each window is a subsequence of the underlying collection. In the example in the lecture,ArraySlice, to avoid intermediate allocation.

import Algorithms

let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for window in x.windows(ofCount: 3) {
    print(window)
}

// Prints [0, 1, 2]
// Prints [1, 2, 3]
// Prints [2, 3, 4]
// Prints [3, 4, 5]

Key points:

  • import AlgorithmsIntroducing the Swift Algorithms package. -xIs the array to be traversed. -windows(ofCount: 3)Generate overlapping windows of length 3. -windowis a contiguous slice of the original collection.
  • Adjacent windows share elements, e.g.[0, 1, 2]behind is[1, 2, 3]

(07:30) Windows of length 2 are common, so the package providesadjacentPairs(). It returns a tuple, and accessing the two elements before and after is more straightforward.

import Algorithms

let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for (prev, next) in x.adjacentPairs() {
    print((prev, next))
}

// Prints (0, 1)
// Prints (1, 2)
// Prints (2, 3)
// Prints (3, 4)

Key points:

  • adjacentPairs()Iterate over pairs of adjacent elements. -(prev, next)Deconstruct the previous and next elements directly.
  • Suitable for comparing adjacent values, such as detecting date intervals, trend changes, and continuous repetitions.

07:45chunks(ofCount:)Group by fixed size. Unlike windows, the blocks do not overlap.

import Algorithms

let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for chunk in x.chunks(ofCount: 3) {
    print(chunk)
}

// Prints [0, 1, 2]
// Prints [3, 4, 5]
// Prints [6, 7, 8]
// Prints [9]

Key points:

  • chunks(ofCount: 3)Generate a set of every 3 elements.
  • eachchunkis a subsequence of the underlying collection.
  • When the last group contains less than 3 elements, the remaining elements will be included.

08:49chunkedYou can also group by custom conditions. Lecture uses it to insert timestamps into chat records: if the interval between adjacent transcript items is less than one hour, they are placed in the same group; inserting between groupsDateItem

import Algorithms

extension Message {
    func makeMessageParts() -> [TranscriptItem]
}

transcript = Array(
    messages
        .lazy
        .flatMap { $0.makeMessageParts() }
        .chunked { $1.date.timeIntervalSince($0.date) < 60 * 60 }
        .joined { DateItem(date: $1.first!.date) }
)

Key points:

  • messages.lazyDelayed processing of message array. -flatMap { $0.makeMessageParts() }Convert messages into a flat stream of transcript items. -chunked { ... }Receive two adjacent elements$0and$1
  • $1.date.timeIntervalSince($0.date) < 60 * 60Indicates that the interval between two items is less than one hour. -joined { DateItem(date: $1.first!.date) }Inserts timestamps based on the first date of the next group between adjacent groups. -Array(...)Save the final transcript to avoid subsequent double counting.

Swift Collections: Deque is suitable for operations on both ends

14:56DequeIs a double-ended queue (double-ended queue). It supports appending from the tail and removing from the head, as well as inserting from the head and removing from the tail.

import Collections

var queue: Deque = ["A", "B", "C"]

queue.append("D")
queue.append("E")
queue.removeFirst()  // "A"
queue.removeFirst()  // "B"

queue.prepend("F")
queue.prepend("G")
queue.removeLast()   // "E"
queue.removeLast()   // "D"

Key points:

  • import CollectionsIntroducing the Swift Collections package. -var queue: Deque = ["A", "B", "C"]Create a deque using an array literal. -appendAdd elements from the end of the queue. -removeFirstRemoves the element from the head of the queue. -prependAdd elements from the head of the queue. -removeLastRemoves elements from the end of the queue.

15:46DequeThe API is close toArray. it complies withRandomAccessCollectionMutableCollectionandRangeReplaceableCollection, you can access, modify, and insert ranges by integer subscript.

import Collections

var items: Deque = ["D", "E", "f"]
print(items[1])  // "E"
items[2] = "F"
items.insert(contentsOf: ["A", "B", "C"], at: 0)
print(items[1])  // "B"

Key points:

  • items[1]Read elements by position. -items[2] = "F"Modify elements by subscripting. -insert(contentsOf:at:)Inserts an element at the specified position. -at: 0Indicates inserting at the beginning.
  • Speech Notes,DequeThis will cause the storage buffer to bypass the boundary, so initial insertion does not require moving existing elements, and the time is approximately constant.

18:39DequeWhen deleting a range in the middle, you can choose to move the first or second half elements to fill the hole. The talk concluded that random position deletion is on average two times faster.

import Collections

var items: Deque = ["A", "B", "C", "D", "E", "F"]
items.removeSubrange(1 ..< 3)

Key points:

  • itemsInitially contains 6 elements. -1 ..< 3Represents the range from subscript 1 to subscript 3. -removeSubrangedelete"B"and"C"
  • DequeYou can choose to move the less side to close the deleted hole.

OrderedSet: uniqueness and order are established at the same time

(19:33) StandardSetThe elements are guaranteed to be unique, but the order of the elements is meaningless. Two sets containing the same elements may be printed in different order and still compare equal.

let first: Set = ["A", "B", "C", "D", "E", "F"]
print(first)  // ["B", "E", "C", "F", "D", "A"]
let second: Set = ["A", "B", "C", "D", "E", "F"]
print(second)  // ["A", "D", "E", "F", "C", "B"]
print(first == second)  // true

Key points:

  • SetIt only cares whether the members are the same. -print(first)andprint(second)The order may be different. -first == secondstilltrue.
  • This model is suitable for scenes that only require weight removal.

20:26OrderedSetSave insertion order. Participate in equality comparison sequentially; if you only care about the member collection, you can useunorderedview.

import Collections

let first: OrderedSet = ["A", "B", "C", "D", "E", "F"]
print(first)         // ["A", "B", "C", "D", "E", "F"]

let second: OrderedSet = ["F", "E", "D", "C", "B", "A"]

print(first == second)                      // false
print(first.unordered == second.unordered)  // true

Key points:

  • OrderedSetCan be created using array literals. -print(first)Preserve order in literals. -first == secondCompare members and order. -unorderedThe view ignores order and only compares members.

21:04OrderedSetSupports integer subscripting, sorting and shuffling like an array, and avoids duplicate elements like a set.

import Collections

var items: OrderedSet = ["E", "D", "C", "B", "A"]
items[3]  // "B"
items.append("F")         // (inserted: true, index: 5)
items.insert("B", at: 1)  // (inserted: false, index: 3)
items.remove("E")
items.sort()
items.shuffle()

Key points:

  • items[3]Access elements by position. -append("F")Inserts a new element and returns whether it was inserted and the element index. -insert("B", at: 1)Discover"B"already exists, so the existing subscript is returned. -remove("E")Delete by element. -sort()andshuffle()Perform a reordering operation.

OrderedDictionary: Search by key, also retain the order of key-value pairs

26:46OrderedDictionaryIt is an ordered version of the dictionary. It uses key to check value and maintains the certain order of key-value pairs. The default is the insertion order of key.

import Collections

var dict: OrderedDictionary = [2: "two", 1: "one", 0: "zero"]

print(dict[1])  // Optional("one")
print(dict)     // [2: "two", 1: "one", 0: "zero"]

dict[3] = "three"
dict[1] = nil
print(dict)     // [2: "two", 0: "zero", 3: "three"]

Key points:

  • OrderedDictionaryCan be created using dictionary literals. -dict[1]Read value by key. -print(dict)Display the determined sequence of key-value pairs. -dict[3] = "three"Append elements for the new key. -dict[1] = nilDelete existing keys.

27:38OrderedDictionaryThe subscript always represents the key subscript. To access key-value pairs by location, you need to goelementsview.

import Collections

var dict: OrderedDictionary = [2: "two", 0: "zero", 3: "three"]

print(dict[0])           // Optional("zero")

print(dict.elements[0])  // (key: 2, value: "two")

Key points:

  • dict[0]Find key for0value.
  • This operation returnsOptional("zero")
  • dict.elementsIs a random access collection view. -dict.elements[0]Returns the key-value pair at position 0.
  • Speech Notes,OrderedDictionaryitself only conforms toSequence, used when collective behavior is requiredelements

Core Takeaways

  1. What to do: Add “Insert date separator bar by time interval” to the chat app.

    • Why it’s worth doing: Swift AlgorithmschunkedandjoinedThe process of “grouping by adjacent elements and then inserting separators between groups” has been covered.
    • How ​​to start: Expose the message itemdate,use.chunked { $1.date.timeIntervalSince($0.date) < 60 * 60 }Group and reuse.joined { DateItem(date: $1.first!.date) }Insert timestamp.
  2. What to do: Implement a recently viewed photo bar that only displays the last 6 photos.

    • Why it’s worth doing: Standard library algorithm chain can replace handwritten reverse loop,.lazyIntermediate arrays can be reduced.
    • How ​​to start: From transcript.lazy.compactMap { $0 as? PhotoItem }.suffix(6).reversed(), and finally useArray(...)Save the results.
  3. What to do: Change the downloader or task system to a double-ended task queue.

    • Why it’s worth doing:DequeIt is more suitable for head and tail insertion and deletion. Beginning insertion does not require moving existing elements.
    • How ​​to start: UseDeque<Task>Save pending tasks; normal tasksappend, high priority tasksprepend, when consuming tasksremoveFirst()
  4. What to do: Make a drag-and-drop sortable tag manager.

    • Why it’s worth doing:OrderedSetExpressing uniqueness and user-specified order at the same time, it is suitable for UIs where “labels cannot be repeated, but the order must be saved”.
    • How ​​to start: UseOrderedSet<Tag>As a data source; for adding new tagsappend;Calling sorting or moving logic after dragging; used when only comparing members.unordered
  5. What to do: Build a sequentially stable configuration editor.

    • Why it’s worth doing:OrderedDictionaryYou can quickly check the value by key, and display the configuration items in the order of insertion.
    • How ​​to start: UseOrderedDictionary<String, SettingValue>Save the configuration; read the value withdict[key];For list displaydict.elements

Comments

GitHub Issues · utterances