Dictionary Ergonomics with Identifiable Arrays

1import Foundation
2
3Introduction
4
5In Swift, arrays and dictionaries are fundamental tools for data
6manipulation.
7
8While dictionaries offer ergonomic access and modification via keys, 
9arrays are the most common type in applications retrieving data 
10from remote APIs or local storage solutions.
11
12In this article, we will explore array manipulation enhancing using custom
13subscripts to achieve dictionnary ergonomics.
14
15Dictionary Manipulation
16
17In Swift, manipulating dictionary data is pretty straightforward. 
18
19For example, if we have a model like this:
20
21struct Model {
22    let id: Int 
23    var name: String?
24}
25
26And a dictionary whose keys match the `id` type of that model:
27
28var dict = [Int: Model]()
29
30We can perform "CRUD" operations quite easily:
31
32Note: While this example uses the term "CRUD" 
33(Create, Read, Update, Delete), it's applied here more conceptually
34rather than in its traditional database context.
35
36C: dict[1] = Model(id: 1, name: "Some name")
37R: _ = dict[1] 
38U: dict[1]?.name = "New name"
39D: dict[1] = nil
40
41Array Manipulation
42
43With an array, manipulating the data is more verbose:
44
45var array = [Model]()
46
47C: array.append(Model(id: 1, name: "Some name"))
48R: _ = array.filter { $0.id == 1 }.first
49R: 
50if let idx = array.firstIndex(where: { $0.id == 1 }) {
51    _ = array[idx]
52}
53
54U:
55if let idx = array.firstIndex(where: { $0.id == 1 }) {
56    array[idx].name = "Some name"
57}
58
59D:
60if let idx = array.firstIndex(where: { $0.id == 1 }) {
61    array.remove(at: idx)
62}
63
64Using a Custom Subscript
65
66As we can see, the ergonomics of a dictionary are much more practical 
67and concise, especially in update and delete cases, 
68where with an array, we need a preliminary step to retrieve the index 
69of the element to update/delete.
70
71Swift allows us to create custom `subscript` methods for 
72manipulating collections.
73
74With this functionality, we can achieve an array manipulation API 
75identical to that of a dictionary.
76
77The only requirement is that the elements of our array conform to the 
78`Identifiable` protocol.
79
80Implementation
81
82The subscript encapsulates the boilerplate that we usually
83manually declare when performing crud operations against an array:
84
85extension Array where Element: Identifiable {
86    
87    It uses the element IDs as keys...
88    
89    subscript(id: Element.ID) -> Element? {
90        
91        ... and just like a dictionary, returns an optional...
92        
93        get { first { $0.id == id } }
94        set(newValue) {
95            
96            Create/Update/Delete:
97            
98            if let index = firstIndex(where: { $0.id == id }) {
99                if let newValue = newValue {
100                    self[index] = newValue
101                } else {
102                    remove(at: index)
103                }
104            } else if let newValue = newValue {
105                append(newValue)
106            }
107        }
108    }
109}
110
111Usage:
112
113struct Todo: Identifiable {
114    let id = UUID()
115    var description: String
116    var isChecked = false
117}
118
119final class TodoStore: ObservableObject {
120    
121    @Published private(set) var data = [Todo]()
122    
123    😎 With subscript:
124    
125    func check_1(_ id: UUID) {
126        data[id]?.isChecked.toggle()
127    }
128    
129    😫 Without subscript:
130    
131    func check_2(_ id: UUID) {
132        if let index = data.firstIndex(where: { $0.id == id }) {
133            data[index].isChecked.toggle()
134        }
135    }
136    
137    🎉 More examples:
138    
139    func upsert(item: Todo) {
140        data[item.id] = item
141    }
142    
143    func read(id: UUID) -> Todo? {
144        data[id]
145    }
146    
147    func delete(id: UUID) {
148        data[id] = nil
149    }
150}
151
152
153Conclusion
154
155This small snippet offers several advantages, including:
156•	Reduction of repetitive code: Removes the need to manually search for indices in common operations.
157•	Ergonomics: Provides an API similar to a dictionary.

159
160
161Tests
162
163Run with `alt`+ `R` 😉
164
165final class Tests {
166    struct User: Identifiable, Equatable {
167        var id = UUID()
168        var firstName: String
169        var lastName: String
170    }
171    
172    func test_create() {
173        let user = User(firstName: "Cristian", lastName: "Patiño")
174        var sut = [User]()
175        sut[user.id] = user
176        assert(sut[user.id]?.firstName == "Cristian")
177    }
178    
179    func test_update() {
180        var user = User(firstName: "Cristian", lastName: "Patiño")
181        var sut = [User]()
182        sut[user.id] = user
183        
184        Update
185        user.firstName = "Cristian Felipe"
186        sut[user.id] = user
187        
188        assert(sut[user.id]?.firstName == "Cristian Felipe")
189    }
190    
191    func test_delete() {
192        let user = User(firstName: "Cristian", lastName: "Patiño")
193        var sut = [User]()
194        sut[user.id] = user
195        assert(sut[user.id] != nil)
196        sut[user.id] = nil
197        assert(sut[user.id] == nil)
198    }
199    
200    func run() {
201        test_create()
202        test_update()
203        test_delete()
204    }
205    
206    func assert(
207        _ b: Bool,
208        line: UInt = #line,
209        function: String = #function
210    ) {
211        let emoji = b ? "✅" : "❌"
212        print(line, emoji + " " + function)
213    }
214}
215
216Tests().run()
217
 ✅ test_create()
 ✅ test_update()
 ✅ test_delete()
 ✅ test_delete()