1import Foundation
23Introduction45In Swift, arrays and dictionaries are fundamental tools for data
6manipulation.
78While 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.
1112In this article, we will explore array manipulation enhancing using custom
13subscripts to achieve dictionnary ergonomics.
1415Dictionary Manipulation1617In Swift, manipulating dictionary data is pretty straightforward.
1819For example, if we have a model like this:2021structModel {
22letid: Int23varname: String?
24}
2526And a dictionary whose keys match the `id` type of that model:2728vardict = [Int: Model]()
2930We can perform "CRUD" operations quite easily:
3132Note: While this example uses the term "CRUD"
33(Create, Read, Update, Delete), it's applied here more conceptually34rather than in its traditional database context.3536C: dict[1] = Model(id: 1, name: "Some name")
37R:_ = dict[1]
38U: dict[1]?.name = "New name"39D: dict[1] = nil4041Array Manipulation4243With an array, manipulating the data is more verbose:4445vararray = [Model]()
4647C: 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}
5354U:55if let idx = array.firstIndex(where: { $0.id == 1 }) {
56 array[idx].name = "Some name"57}
5859D:60if let idx = array.firstIndex(where: { $0.id == 1 }) {
61 array.remove(at: idx)
62}
6364Using a Custom Subscript6566As 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.
7071Swift allows us to create custom `subscript` methods for
72manipulating collections.
7374With this functionality, we can achieve an array manipulation API
75identical to that of a dictionary.
7677The only requirement is that the elements of our array conform to the
78`Identifiable` protocol.
7980Implementation8182The subscript encapsulates the boilerplate that we usually
83manually declare when performing crud operations against an array:8485extensionArraywhereElement: Identifiable {
8687 It uses the element IDs as keys...8889subscript(id: Element.ID) -> Element? {
9091 ... and just like a dictionary, returns an optional...9293get { first { $0.id == id } }
94set(newValue) {
9596Create/Update/Delete:9798if let index = firstIndex(where: { $0.id == id }) {
99if let newValue = newValue {
100self[index] = newValue
101 } else {
102remove(at: index)
103 }
104 } else if let newValue = newValue {
105append(newValue)
106 }
107 }
108 }
109}
110111Usage:112113structTodo: Identifiable {
114letid = UUID()
115vardescription: String116varisChecked = false117}
118119final classTodoStore: ObservableObject {
120121@Published private(set) var data = [Todo]()
122123😎 With subscript:124125funccheck_1(_ id: UUID) {
126 data[id]?.isChecked.toggle()
127 }
128129😫 Without subscript:130131funccheck_2(_ id: UUID) {
132if let index = data.firstIndex(where: { $0.id == id }) {
133 data[index].isChecked.toggle()
134 }
135 }
136137🎉 More examples:138139funcupsert(item: Todo) {
140 data[item.id] = item
141 }
142143funcread(id: UUID) -> Todo? {
144 data[id]
145 }
146147funcdelete(id: UUID) {
148 data[id] = nil149 }
150}
151152153Conclusion154155This 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.
159160161Tests162163Run with `alt`+ `R` 😉164165final classTests {
166structUser: Identifiable, Equatable {
167varid = UUID()
168varfirstName: String169varlastName: String170 }
171172functest_create() {
173letuser = User(firstName: "Cristian", lastName: "Patiño")
174varsut = [User]()
175 sut[user.id] = user
176assert(sut[user.id]?.firstName == "Cristian")
177 }
178179functest_update() {
180varuser = User(firstName: "Cristian", lastName: "Patiño")
181varsut = [User]()
182 sut[user.id] = user
183184Update185 user.firstName = "Cristian Felipe"186 sut[user.id] = user
187188assert(sut[user.id]?.firstName == "Cristian Felipe")
189 }
190191functest_delete() {
192letuser = User(firstName: "Cristian", lastName: "Patiño")
193varsut = [User]()
194 sut[user.id] = user
195assert(sut[user.id] != nil)
196 sut[user.id] = nil197 assert(sut[user.id] == nil)
198 }
199200funcrun() {
201test_create()
202test_update()
203test_delete()
204 }
205206funcassert(
207_ b: Bool,
208 line: UInt = #line,
209 function: String = #function210 ) {
211letemoji = b ? "✅" : "❌"212print(line, emoji + " " + function)
213 }
214}
215216Tests().run()
217