• Stars
    star
    529
  • Rank 83,810 (Top 2 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 3 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A library of data structures for working with collections of identifiable elements in an ergonomic, performant way.

Swift Identified Collections

CI Slack

A library of data structures for working with collections of identifiable elements in an ergonomic, performant way.

Motivation

When modeling a collection of elements in your application's state, it is easy to reach for a standard Array. However, as your application becomes more complex, this approach can break down in many ways, including accidentally making mutations to the wrong elements or even crashing. ๐Ÿ˜ฌ

For example, if you were building a "Todos" application in SwiftUI, you might model an individual todo in an identifiable value type:

struct Todo: Identifiable {
  var description = ""
  let id: UUID
  var isComplete = false
}

And you would hold an array of these todos as a published field in your app's view model:

class TodosViewModel: ObservableObject {
  @Published var todos: [Todo] = []
}

A view can render a list of these todos quite simply, and because they are identifiable we can even omit the id parameter of List:

struct TodosView: View {
  @ObservedObject var viewModel: TodosViewModel
  
  var body: some View {
    List(self.viewModel.todos) { todo in
      ...
    }
  }
}

If your deployment target is set to the latest version of SwiftUI, you may be tempted to pass along a binding to the list so that each row is given mutable access to its todo. This will work for simple cases, but as soon as you introduce side effects, like API clients or analytics, or want to write unit tests, you must push this logic into a view model, instead. And that means each row must be able to communicate its actions back to the view model.

You could do so by introducing some endpoints on the view model, like when a row's completed toggle is changed:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at id: Todo.ID) {
    guard let index = self.todos.firstIndex(where: { $0.id == id })
    else { return }
    
    self.todos[index].isComplete.toggle()
    // TODO: Update todo on backend using an API client
  }
}

This code is simple enough, but it can require a full traversal of the array to do its job.

Perhaps it would be more performant for a row to communicate its index back to the view model instead, and then it could mutate the todo directly via its index subscript. But this makes the view more complicated:

List(self.viewModel.todos.enumerated(), id: \.element.id) { index, todo in
  ...
}

This isn't so bad, but at the moment it doesn't even compile. An evolution proposal may change that soon, but in the meantime List and ForEach must be passed a RandomAccessCollection, which is perhaps most simply achieved by constructing another array:

List(Array(self.viewModel.todos.enumerated()), id: \.element.id) { index, todo in
  ...
}

This compiles, but we've just moved the performance problem to the view: every time this body is evaluated there's the possibility a whole new array is being allocated.

But even if it were possible to pass an enumerated collection directly to these views, identifying an element of mutable state by an index introduces a number of other problems.

While it's true that we can greatly simplify and improve the performance of any view model methods that mutate an element through its index subscript:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at index: Int) {
    self.todos[index].isComplete.toggle()
    // TODO: Update todo on backend using an API client
  }
}

Any asynchronous work that we add to this endpoint must take great care in not using this index later on. An index is not a stable identifier: todos can be moved and removed at any time, and an index identifying "Buy lettuce" at one moment may identify "Call Mom" the next, or worse, may be a completely invalid index and crash your application!

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at index: Int) async {
    self.todos[index].isComplete.toggle()
    
    do {
      // โŒ Could update the wrong todo, or crash!
      self.todos[index] = try await self.apiClient.updateTodo(self.todos[index]) 
    } catch {
      // Handle error
    }
  }
}

Whenever you need to access a particular todo after performing some asynchronous work, you must do the work of traversing the array:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at index: Int) async {
    self.todos[index].isComplete.toggle()
    
    // 1๏ธโƒฃ Get a reference to the todo's id before kicking off the async work
    let id = self.todos[index].id
  
    do {
      // 2๏ธโƒฃ Update the todo on the backend
      let updatedTodo = try await self.apiClient.updateTodo(self.todos[index])
              
      // 3๏ธโƒฃ Find the updated index of the todo after the async work is done
      let updatedIndex = self.todos.firstIndex(where: { $0.id == id })!
      
      // 4๏ธโƒฃ Update the correct todo
      self.todos[updatedIndex] = updatedTodo
    } catch {
      // Handle error
    }
  }
}

Introducing: identified collections

Identified collections are designed to solve all of these problems by providing data structures for working with collections of identifiable elements in an ergonomic, performant way.

Most of the time, you can simply swap an Array out for an IdentifiedArray:

import IdentifiedCollections

class TodosViewModel: ObservableObject {
  @Published var todos: IdentifiedArrayOf<Todo> = []
  ...
}

And then you can mutate an element directly via its id-based subscript, no traversals needed, even after asynchronous work is performed:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at id: Todo.ID) async {
    self.todos[id: id]?.isComplete.toggle()
    
    do {
      // 1๏ธโƒฃ Update todo on backend and mutate it in the todos identified array.
      self.todos[id: id] = try await self.apiClient.updateTodo(self.todos[id: id]!)
    } catch {
      // Handle error
    }

    // No step 2๏ธโƒฃ ๐Ÿ˜†
  }
}

You can also simply pass the identified array to views like List and ForEach without any complications:

List(self.viewModel.todos) { todo in
  ...
}

Identified arrays are designed to integrate with SwiftUI applications, as well as applications written in the Composable Architecture.

Design

IdentifiedArray is a lightweight wrapper around the OrderedDictionary type from Apple's Swift Collections. It shares many of the same performance characteristics and design considerations, but is better adapted to solving the problem of holding onto a collection of identifiable elements in your application's state.

IdentifiedArray does not expose any of the details of OrderedDictionary that may lead to breaking invariants. For example an OrderedDictionary<ID, Identifiable> may freely hold a value whose identifier does not match its key or multiple values could have the same id, and IdentifiedArray does not allow for these situations.

And unlike OrderedSet, IdentifiedArray does not require that its Element type conforms to the Hashable protocol, which may be difficult or impossible to do, and introduces questions around the quality of hashing, etc.

IdentifiedArray does not even require that its Element conforms to Identifiable. Just as SwiftUI's List and ForEach views take an id key path to an element's identifier, IdentifiedArrays can be constructed with a key path:

var numbers = IdentifiedArray(id: \Int.self)

Performance

IdentifiedArray is designed to match the performance characteristics of OrderedDictionary. It has been benchmarked with Swift Collections Benchmark:

Community

If you want to discuss this library or have a question about how to use it to solve a particular problem, there are a number of places you can discuss with fellow Point-Free enthusiasts:

Documentation

The latest documentation for Identified Collections' APIs is available here.

Translations

Interested in learning more?

These concepts (and more) are explored thoroughly in Point-Free, a video series exploring functional programming and Swift hosted by Brandon Williams and Stephen Celis.

Usage of IdentifiedArray in the Composable Architecture was explored in the following Point-Free episode:

video poster image

License

All modules are released under the MIT license. See LICENSE for details.

More Repositories

1

swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
Swift
12,150
star
2

swift-snapshot-testing

๐Ÿ“ธ Delightful Swift snapshot testing.
Swift
3,737
star
3

isowords

Open source game built in SwiftUI and the Composable Architecture.
Swift
2,667
star
4

swift-navigation

Bringing simple and powerful navigation tools to all Swift platforms, inspired by SwiftUI.
Swift
1,950
star
5

swift-dependencies

A dependency management library inspired by SwiftUI's "environment."
Swift
1,527
star
6

swift-tagged

๐Ÿท A wrapper type for safer, expressive code.
Swift
1,364
star
7

swift-overture

๐ŸŽผ A library for function composition.
Swift
1,139
star
8

pointfreeco

๐ŸŽฌ The source for www.pointfree.co, a video series on functional programming and the Swift programming language.
Swift
1,098
star
9

episode-code-samples

๐Ÿ’พ Point-Free episode code.
Swift
950
star
10

swift-case-paths

๐Ÿงฐ Case paths extends the key path hierarchy to enum cases.
Swift
905
star
11

swift-parsing

A library for turning nebulous data into well-structured data, with a focus on composition, performance, generality, and ergonomics.
Swift
847
star
12

swift-nonempty

๐ŸŽ A compile-time guarantee that a collection contains a value.
Swift
839
star
13

swift-custom-dump

A collection of tools for debugging, diffing, and testing your application's data structures.
Swift
795
star
14

swift-html

๐Ÿ—บ A Swift DSL for type-safe, extensible, and transformable HTML documents.
Swift
760
star
15

combine-schedulers

โฐ A few schedulers that make working with Combine more testable and more versatile.
Swift
701
star
16

swift-perception

Observable tools, backported.
Swift
536
star
17

swift-web

๐Ÿ•ธ A collection of Swift server-side frameworks for handling HTML, CSS, routing and middleware.
Swift
481
star
18

swift-prelude

๐ŸŽถ A collection of types and functions that enhance the Swift language.
Swift
469
star
19

swift-validated

๐Ÿ›‚ A result type that accumulates multiple errors.
Swift
392
star
20

swift-issue-reporting

Report issues in your application and library code as Xcode runtime warnings, breakpoints, assertions, and do so in a testable manner.
Swift
361
star
21

swift-url-routing

A bidirectional router with more type safety and less fuss.
Swift
347
star
22

swift-concurrency-extras

Useful, testable Swift concurrency.
Swift
315
star
23

swift-gen

๐ŸŽฑ Composable, transformable, controllable randomness.
Swift
266
star
24

swift-macro-testing

Magical testing tools for Swift macros.
Swift
263
star
25

swift-clocks

โฐ A few clocks that make working with Swift concurrency more testable and more versatile.
Swift
258
star
26

syncups

A rebuild of Appleโ€™s โ€œScrumdingerโ€ application using modern, best practices for SwiftUI development.
Swift
205
star
27

swift-enum-properties

๐Ÿค Struct and enum data access in harmony.
Swift
200
star
28

composable-core-location

A library that bridges the Composable Architecture and Core Location.
Swift
108
star
29

vapor-routing

A bidirectional Vapor router with more type safety and less fuss.
Swift
89
star
30

swift-html-vapor

๐Ÿ’ง Vapor plugin for type-safe, transformable HTML views.
Swift
84
star
31

swift-playground-templates

๐Ÿซ A collection of helpful Xcode playground templates.
Makefile
81
star
32

pointfreeco-server

Point-Free server code.
40
star
33

TrySyncUps

The starting project for our try! Swift 2024 Composable Architecture workshop.
Swift
38
star
34

composable-core-motion

A library that bridges the Composable Architecture and Core Motion.
Swift
29
star
35

swift-boundaries

๐Ÿฃ Functional core, imperative shell.
Swift
28
star
36

swift-quickcheck

๐Ÿ An implementation of QuickCheck in Swift.
Swift
25
star
37

swift-either

For those times you want A or B!
Swift
21
star
38

swift-algebras

Algebraic laws bundled into concrete data types.
20
star
39

swift-parser-printer

โ†”๏ธ Parsing and printing
Swift
15
star
40

swift-html-kitura

โ˜๏ธ Kitura plugin for type-safe, transformable HTML views.
Swift
14
star
41

swiftui-navigation

This package is now Swift Navigation:
Swift
13
star
42

homebrew-swift

Ruby
3
star
43

swift-bugs

3
star
44

Ccmark

Swift
2
star
45

.github

1
star