Swiftpack.co -  dzmitry-antonenka/SwiftTreeDataSource as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
The purpose of the package to provide utils to handle hierachical data structures to help visualize them in lists-like way. No dependencies to UI frameworks.
.package(url: "https://github.com/dzmitry-antonenka/SwiftTreeDataSource.git", from: "1.0.0")


The purpose of the package to provide utils to handle hierachical data structures to help visualize them in lists-like way. No dependecies to UI frameworks.


TreeView (iOS): FileViewer (macOS):

Note: FileViewer is a modification for [RW tutorial] (https://www.raywenderlich.com/830-macos-nstableview-tutorial)



Add additional entry to your Podfile.

pod "SwiftTreeDataSource", "1.0.0"

Swift Package Manager. Works along with CocoaPods and others! You can add it directly in Xcode. File -> Swift Packages -> Add Package Dependency -> .. Repo URL: https://github.com/dzmitry-antonenka/SwiftTreeDataSource.git


It only depends on Swift 5.3 & Dispatch framework, according to docs minimal support apple platforms:

  • iOS '8.0'
  • macOS '10.10'
  • tvOS '9.0'
  • watchOS '2.0'


Please see example projects for more!

Create data source:

var dataSource = TreeDataSource<OutlineItem>()

// create data source with filtering support.
var filterableTreeDataSource = FilterableTreeDataSource<OutlineItem>()

Add/Insert/Delete methods:

// Helper to traverse items with all nested children to include to data source. 
addItems(items, itemChildren: { $0.subitems }, to: dataSource)

// Append:
dataSource.append(currentToAdd, to: referenceParent)

// Insert:
dataSource.insert([insertionBeforeItem], before: existingItem)
dataSource.insert([insertionAfterItem], after: existingItem)

// Delete:

UI related logic:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return treeDataSource.items.count

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell

    let item = dataSource.items[indexPath.row]

    let left = 11 + 10 * item.level
    cell.lbl?.text = item.value.title
    cell.lblLeadingConstraint.constant = CGFloat(left)
    cell.disclosureImageView.isHidden = item.subitems.isEmpty

    let transform = CGAffineTransform.init(rotationAngle: item.isExpanded ? CGFloat.pi/2.0 : 0)
    cell.disclosureImageView.transform = transform

    return cell

// MARK: - Example with fitering

func performSearch(with searchText: String) {
    if !searchText.isEmpty {
        self.searchBar.isLoading = true
        self.displayMode = .filtering(text: searchText)

        self.filterableTreeDataSource.filterItemsKeepingParents(by: { $0.title.lowercased().contains(searchText.lowercased()) }) { [weak self] in
            guard let self = self else { return }
            self.searchBar.isLoading = false
            self.reloadUI(animating: false)
    } else {
        self.displayMode = .standard
        self.searchBar.isLoading = false
        self.filterableTreeDataSource.resetFiltering(collapsingAll: true)
        self.reloadUI(animating: false)

func reloadUI(animating: Bool = true) {
    if isOS13Available {
        var diffableSnaphot = NSDiffableDataSourceSnapshot<Section, TreeDataSource<OutlineItem>.TreeItemType>()
        diffableSnaphot.appendItems(filterableTreeDataSource.items, toSection: .main)
        self.diffableDataSource.apply(diffableSnaphot, animatingDifferences: animating)
    } else {

More advanced formula to try to fit indentation into available space, helped colleagues with data science background:

/// https://en.wikipedia.org/wiki/Exponential_decay
let isPad = UIDevice.current.userInterfaceIdiom == .pad
let decayCoefficient: Double = isPad ? 150 : 40
let lvl = Double(item.level)
let left = 11 + 10 * lvl * ( exp( -lvl / max(decayCoefficient, lvl)) )
let leftOffset: CGFloat = min( CGFloat(left) , UIScreen.main.bounds.width - 210.0)
cell.lblLeadingConstraint.constant = leftOffset

Why yet another library? When we have quite a few:

Key differences:

  • Support for older OS versions that can work with Swift 5.3 & Dispatch (GCD) framework.
  • This solution is UI agnostic and doesn't depend on any UI components or frameworks. You can use for UITableView, NSTableView (macOS), UICollectionView or use other custom frameworks.
  • Included support for filtering.

Implementation details summary:

The key algorithm used is depth first traversal (also called DFS) to build flattened store of nodes and expose it to consumer. Essentially what we need is flat list, node nesting level is used to add identation decoration. Component that supports filtering uses same strategy, but with small modifications: DSF with filtering. Implemented in iterative style to achieve maximum performance. Please see source code for details.


The performance of underlying components is quite decent and achieved with iterative style implementation. For large data set performance issues migth be caused by consumer UI frameworks, e.g. long diffing by NSDiffableDataSourceSectionSnapshot when processing lots of data. In this case please disable animation when appying differences or fall back to reloadData.

Testing version 1.0.0 on MacBook Pro 2019:

Expand All Levels (worst case since requires to traverse all nodes, method expandAll()):

  • ~88_000 nodes: 0.133 sec.
  • ~350_000 nodes: 0.479 sec.
  • ~797_000 nodes: 1.149 sec.
  • ~7_200_000 nodes: 10.474 sec.

Add items to data source (method addItems(_:to:)):

  • ~88_000 nodes: 0.483 sec.
  • ~350_000 nodes: 1.848 sec.
  • ~797_000 nodes: 4.910 sec.
  • ~7_200_000 nodes: 46.816 sec.

Unit testing:

All functional items are under test, but there might be some room for improvement (e.g. covering additional corner cases).


// to get `testable` access to internal stuff, including to `backingStore`.
@testable import SwiftTreeDataSource

// Make conform to `CustomDebugStringConvertible`.
extension OutlineItem: CustomDebugStringConvertible {
    public var debugDescription: String { "\(title)" }

// Correct usage:
let output1 = debugDescriptionTopLevel(dataSource.items) // ✅ Description for top level since `items` already flattened (one-level perspective) and include expanded children.
let output2 = debugDescriptionAllLevels(dataSource.backingStore) // ✅ Description for all levels, asked with hierarchical store as input.
let output3 = debugDescriptionExpandedLevels(dataSource.backingStore) // ✅ Description for expanded levels, asked with hierarchical store as input.
print(output3) // print output to console for example.  

// Wrong usage:
// ❌ `dataSource.items` are already flattened (one-level perspetive) and include expanded items, so methods below will return nonsense.
debugDescriptionAllLevels(dataSource.items) // ❌
debugDescriptionExpandedLevels(dataSource.items) // ❌


Stars: 0
Last commit: 1 week ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API