인체 공학적이고, 성능이 뛰어난 방식으로 indentifiable한 element의 모음으로 작업하기 위한 데이터 구조 라이브러리 입니다.
당신의 어플리케이션의 상태에서 elements들의 collection을 모델링할 때, standard한 Array
에 도달하기 쉽습니다. 그러나, 어플리케이션이 복잡해질수록, 이러한 접근은 실수로 잘못 mutating하거나, 심지어 crasing을 만드는 것을 포함한 많은 방식으로 깨질 수 있습니다. 😬
예를 들어, Todos
앱을 SwiftUI로 만든다고 했을 때, 다음과 같이 identificable value type으로 개별적인 todo를 을 선언할 수 있다:
struct Todo: Identifiable {
var description = ""
let id: UUID
var isComplete = false
}
그리고 당신의 앱의 view model은 published field로 이러한 todo의 배열을 가지고 있을 수 있다.
class TodosViewModel: ObservableObject {
@Published var todos: [Todo] = []
}
view는 꽤 간단하게 이러한 todo의 배열을 render할 수 있고, todos는 identifiable하기 때문에, 우리는 List
의 id
파라미터를 생략할 수 있다.
struct TodosView: View {
@ObservedObject var viewModel: TodosViewModel
var body: some View {
List(self.viewModel.todos) { todo in
...
}
}
}
만약 당신의 deployment target이 SwiftUI의 최신 버전으로 설정되어있다면, list에 binding을 전달하여 각각의 row에 todo에 대한 변경 가능한 권한을 부여하고 싶은 유혹에 빠질 수 있다. 이 것은 간단한 케이스에는 동작하지만, API client나 analtyics 같은 side effect를 도입하거나, unit test를 작성하려는 즉시 이 로직을 view model로 푸시해야합니다. 이는 각 row가 해당 action들을 view model로 다시 전달할 수 있어야함을 의미합니다.
row의 완료된 tolggle이 바뀐것과 같이, view model의 몇몇 endpoint를 도입함으로 그렇게 할 수 있습니다.
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
}
}
이 코드는 충분히 간단하지만, 작업을 수행하려면 모든 배열을 순회해야한다.
아마도 row가 index를 view model에 다시 전달하는 것이 성능이 더 좋을 것입니다. 그런 다음 index subscript를 통해 todo를 직접 mutate할 수 있지만, 이것은 view를 더 복잡하게 만듭니다.
List(self.viewModel.todos.enumerated(), id: \.element.id) { index, todo in
...
}
이것은 그렇게 나쁘진 않지만, 그 순간에는 심지어 컴파일 되지도 않습니다. evolution proposal 에서 곧 고쳐지겠지만, 그동안 List
와 ForEach
는 반드시 RandomAccessCollection
을 전달해야하지만, 다른 배열을 구성하면 가장 간단하게 달성할 수 있습니다.
List(Array(self.viewModel.todos.enumerated()), id: \.element.id) { index, todo in
...
}
이건 컴파일이 되지만, 우리는 performance 문제를 view로 옮겼다: 매 번 이 body는 evaludate될 때마다 완전히 새로운 배열에 할당될 가능성이 있습니다.
하지만 이 view들에게 직접 enumerated collection을 전달할 수 있다 하더라도, mutable state의 element를 index를 보고 식별하는 것은 많은 문제를 야기할 수 있습니다.
index subscript를 통해 element를 변경하는 view model method의 성능을 크게 단순화하고 향상시킬 수 있는 것은 사실입니다:
class TodosViewModel: ObservableObject {
...
func todoCheckboxToggled(at index: Int) {
self.todos[index].isComplete.toggle()
// TODO: Update todo on backend using an API client
}
}
이 endpoint를 추가하는 모든 비동기 작업은 나중에 이 index를 사용하지 않도록 각별히 주의해야합니다. index는 안정적인 식별자가 아닙니다: todos는 아무 시점에 이동하거나 제거될수 있고, 한 순간데 “상추 구매”를 식별하는 index가 “엄마한테 전화하기” index로 식별될 수 있으며, 최악의 경우 완전히 잘못된 index가 되어 앱의 crash를 일으킬 수도 있습니다!
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
}
}
}
비동기 작업을 수행한 후 특정 todo에 접근해야할 때마다 당신은 반드시 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
}
}
}
Identified collection들은 인체공학적이고, 성능이 뛰어난 방식의 indentifiable element의 collection으로 작업하기 위한 데이터 구조를 제공함으로써 이러한 모든 문제를 해결하기 위해 디자인되었습니다.
대부분의 경우 당신은 간단하게 Array
를 IdentifiedArray
로 교체할 수 있습니다.
import IdentifiedCollections
class TodosViewModel: ObservableObject {
@Published var todos: IdentifiedArrayOf<Todo> = []
...
}
그런 다음 비동기 작업이 수행된 후에도, 순회할 필요 없이, id-based subscript를 통해 직접 element를 변경할 수 있습니다.
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️⃣ 😆
}
}
복잡한 문제 없이 List
및 ForEach
와 같은 view에 indentified array를 전달할 수도 있습니다.
List(self.viewModel.todos) { todo in
...
}
Identified array는 SwiftUI 어플리케이션 및 the Composable Architecture 로 작성된 어플리케이션에 통합되도록 설계되었습니다.
IdentifiedArray
는 Apple의 Swift Collections의 OrderedDictionary
타입을 둘러싼 lightweigt wrapper입니다. 동일한 성능 특성과 설계 고려사항을 많이 공유하지만, 어플리케이션의 상테에서 identifiable element의 collection을 유지하는 문제를 해결하는데 더 적합합니다.
IdentifiedArray
는 불변성을 손상시킬 수 있는 OrderedDictionary
의 세부정보를 노출하지 않습니다. 예를 들어 OrderedDictionary<ID, Identifiable>
는 identifier와 key가 일치하지 않거나, 여러 값이 동일한 id를 가질 수 있지만, IdentifiedArray
는 이러한 상황을 허용하지 않는다.
그리고 OrderSet
과 달리, IdentifiedArray
는 Element
타입이 Hashable
프로토콜을 준수할 것을 요구하지 않습니다. 이는 수행하기 어렵거나 불가능할 수 있으며, hashing 품질에 대한 의문을 도입합니다.
IdentifiedArray
는 해당 Element
가 Identifiable
을 준수할 것 또한 요구하지 않습니다. SwiftUI의 List
및 ForEach
View들이 id
를 key path로 사용하는 것처럼, IdentifiedArray
는 key path를 사용하여 구성할 수 있습니다:
var numbers = IdentifiedArray(id: \Int.self)
IdentifiableArray
는 OrderedDictionary
의 성능 특성과 일치하도록 설계되었습니다. 그것은 Swift Collections Benchmark 로 벤치마킹 되었습니다.
당신은 Identified Collections를 package dependency로 추가하여 Xcode project에 추가할 수 있습니다.
https://github.com/pointfreeco/swift-identified-collections
만약 당신이 Identified Collections를 SwiftPM project에서 사용하려면, Package.swift
에 dependencies
를 추가하면 간단합니다.
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "0.6.0")
],
Identified Collections의 API에 대한 최신 문서는 여기에서 확인할 수 있습니다.
이 컨셉(및 그 이상)은 Brandon Williams 와 Stephen Celis가 주최하는 functional programming 및 Swift를 탐구하는 비디오 시리즈인 Point-Free에서 철저하게 탐구됩니다.
the Composable Architecture에서 IdentifiedArray의 사용은 다음 Point-Free 에피소드에서 살펴볼 수 있습니다:
- Episode 148: Derived Behavior: Collections
모든 모듈은 MIT license로 릴리즈 되었습니다. 자세한 내용은 LICENSE를 참조하세요.