Skip to content

Instantly share code, notes, and snippets.

@havilog
Last active January 18, 2023 05:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save havilog/594bcaba48e7e40cb0395243fa96b47e to your computer and use it in GitHub Desktop.
Save havilog/594bcaba48e7e40cb0395243fa96b47e to your computer and use it in GitHub Desktop.
swift-identified-collections README in korean

Swift Identified Collections

CI

인체 공학적이고, 성능이 뛰어난 방식으로 indentifiable한 element의 모음으로 작업하기 위한 데이터 구조 라이브러리 입니다.

Motivation (동기)

당신의 어플리케이션의 상태에서 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하기 때문에, 우리는 Listid 파라미터를 생략할 수 있다.

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 에서 곧 고쳐지겠지만, 그동안 ListForEach 는 반드시 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
    }
  }
}

Introducing: identified collections

Identified collection들은 인체공학적이고, 성능이 뛰어난 방식의 indentifiable element의 collection으로 작업하기 위한 데이터 구조를 제공함으로써 이러한 모든 문제를 해결하기 위해 디자인되었습니다.

대부분의 경우 당신은 간단하게 ArrayIdentifiedArray 로 교체할 수 있습니다.

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️⃣ 😆
  }
}

복잡한 문제 없이 ListForEach 와 같은 view에 indentified array를 전달할 수도 있습니다.

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

Identified array는 SwiftUI 어플리케이션 및 the Composable Architecture 로 작성된 어플리케이션에 통합되도록 설계되었습니다.

Design (디자인)

IdentifiedArray는 Apple의 Swift CollectionsOrderedDictionary 타입을 둘러싼 lightweigt wrapper입니다. 동일한 성능 특성과 설계 고려사항을 많이 공유하지만, 어플리케이션의 상테에서 identifiable element의 collection을 유지하는 문제를 해결하는데 더 적합합니다.

IdentifiedArray는 불변성을 손상시킬 수 있는 OrderedDictionary 의 세부정보를 노출하지 않습니다. 예를 들어 OrderedDictionary<ID, Identifiable> 는 identifier와 key가 일치하지 않거나, 여러 값이 동일한 id를 가질 수 있지만, IdentifiedArray 는 이러한 상황을 허용하지 않는다.

그리고 OrderSet과 달리, IdentifiedArrayElement 타입이 Hashable 프로토콜을 준수할 것을 요구하지 않습니다. 이는 수행하기 어렵거나 불가능할 수 있으며, hashing 품질에 대한 의문을 도입합니다.

IdentifiedArray 는 해당 ElementIdentifiable 을 준수할 것 또한 요구하지 않습니다. SwiftUI의 ListForEach View들이 id를 key path로 사용하는 것처럼, IdentifiedArray 는 key path를 사용하여 구성할 수 있습니다:

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

Performance (성능)

IdentifiableArrayOrderedDictionary 의 성능 특성과 일치하도록 설계되었습니다. 그것은 Swift Collections Benchmark 로 벤치마킹 되었습니다.

Installation (설치)

당신은 Identified Collections를 package dependency로 추가하여 Xcode project에 추가할 수 있습니다.

https://github.com/pointfreeco/swift-identified-collections

만약 당신이 Identified Collections를 SwiftPM project에서 사용하려면, Package.swiftdependencies 를 추가하면 간단합니다.

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "0.6.0")
],

Documentation (문서)

Identified Collections의 API에 대한 최신 문서는 여기에서 확인할 수 있습니다.

더 알아보고 싶나요?

이 컨셉(및 그 이상)은 Brandon Williams 와 Stephen Celis가 주최하는 functional programming 및 Swift를 탐구하는 비디오 시리즈인 Point-Free에서 철저하게 탐구됩니다.

the Composable Architecture에서 IdentifiedArray의 사용은 다음 Point-Free 에피소드에서 살펴볼 수 있습니다:

video poster image

License (라이센스)

모든 모듈은 MIT license로 릴리즈 되었습니다. 자세한 내용은 LICENSE를 참조하세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment