List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model and doesn't depend on UI framework

Overview

SwiftListTreeDataSource

List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model, so can be used with UITableView/UICollectionView/NSTableView/SwiftUI or in console application.

Demo:

TreeView (iOS): FileViewer (macOS):

Note: FileViewer is a modification for RW tutorial

Installation:

CocoaPods:

Add additional entry to your Podfile.

pod "SwiftListTreeDataSource", "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 -> ..

Requirements:

Swift 5.3 & Dispatch framework. Support for apple platforms starting:

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

Usage:

Please see example projects for more!

Create data source:

var listTreeDataSource = ListTreeDataSource
   
    ()

// create data source with filtering support.
var filterableListTreeDataSource = FilterableListTreeDataSource
    
     ()

    
   

Add/Insert/Delete - quick helper methods:

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

Add/Insert/Delete - More grannular control:

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

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

// Delete:
listTreeDataSource.delete([itemToDelete])

// NOTE: Reload data source at the end of changes.
listTreeDataSource.reload()

UI related logic:

.TreeItemType>() diffableSnaphot.appendSections([.main]) diffableSnaphot.appendItems(filterableTreeDataSource.items, toSection: .main) self.diffableDataSource.apply(diffableSnaphot, animatingDifferences: animating) } else { self.tableView.reloadData() } } ">
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return listTreeDataSource.items.count
}

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

    let item = listTreeDataSource.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
   
    .TreeItemType>()
        diffableSnaphot.appendSections([.main])
        diffableSnaphot.appendItems(filterableTreeDataSource.items, toSection: .main)
        self.diffableDataSource.apply(diffableSnaphot, animatingDifferences: animating)
    } else {
        self.tableView.reloadData()
    }
}

   

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 - 120.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.

Performance

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).

Debugging

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

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

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

// Wrong usage:
// ❌ `listTreeDataSource.items` are already flattened (one-level perspetive) and include expanded items, so methods below will return nonsense.
debugDescriptionAllLevels(listTreeDataSource.items) // ❌
debugDescriptionExpandedLevels(listTreeDataSource.items) // ❌
You might also like...
An easy to use UI component to help display a signal bar with an added customizable fill animation
An easy to use UI component to help display a signal bar with an added customizable fill animation

TZSignalStrengthView for iOS Introduction TZSignalStrengthView is an easy to use UI component to help display a signal bar with an added customizable

Elimination-backoff stack is an unbounded lock-free LIFO linked list, that eliminates concurrent pairs of pushes and pops with exchanges.

Elimination-backoff stack is an unbounded lock-free LIFO linked list, that eliminates concurrent pairs of pushes and pops with exchanges. It uses compare-and-set (CAS) atomic operation to provide concurrent access with obstruction freedom. In order to support even greater concurrency, in case a push/pop fails, it tries to pair it with another pop/push to eliminate the operation through exchange of values.

Reel Search is a Swift UI controller that allows you to choose options from a list
Reel Search is a Swift UI controller that allows you to choose options from a list

REEL SEARCH Reel Search is a Swift UI controller that allows you to choose options from a list We specialize in the designing and coding of custom UI

🔍 Awesome fully customize search view like Pinterest written in Swift 5.0 + Realm support!
🔍 Awesome fully customize search view like Pinterest written in Swift 5.0 + Realm support!

YNSearch + Realm Support Updates See CHANGELOG for details Intoduction 🔍 Awesome search view, written in Swift 5.0, appears search view like Pinteres

Confetti View lets you create a magnificent confetti view in your app
Confetti View lets you create a magnificent confetti view in your app

ConfettiView Confetti View lets you create a magnificent confetti view in your app. This was inspired by House Party app's login screen. Written in Sw

SwiftCrossUI - A cross-platform SwiftUI-like UI framework built on SwiftGtk.

SwiftCrossUI A SwiftUI-like framework for creating cross-platform apps in Swift. It uses SwiftGtk as its backend. This package is still quite a work-i

A fancy hexagonal layout for displaying data like your Apple Watch
A fancy hexagonal layout for displaying data like your Apple Watch

Hexacon is a new way to display content in your app like the Apple Watch SpringBoard Highly inspired by the work of lmmenge. Special thanks to zenly f

A child view controller framework that makes setting up your parent controllers as easy as pie.
A child view controller framework that makes setting up your parent controllers as easy as pie.

Description Family is a child view controller framework that makes setting up your parent controllers as easy as pie. With a simple yet powerful publi

Fashion is your helper to share and reuse UI styles in a Swifty way.
Fashion is your helper to share and reuse UI styles in a Swifty way.

Fashion is your helper to share and reuse UI styles in a Swifty way. The main goal is not to style your native apps in CSS, but use a set

Owner
Dzmitry Antonenka
Dzmitry Antonenka
An easy way to add a shimmering effect to any view with just one line of code. It is useful as an unobtrusive loading indicator.

LoadingShimmer An easy way to add a shimmering effect to any view with just single line of code. It is useful as an unobtrusive loading indicator. Thi

Jogendra 1.4k Jan 4, 2023
A controller that uses a UIStackView and view controller composition to display content in a list

StackViewController Overview StackViewController is a Swift framework that simplifies the process of building forms and other static content using UIS

Seed 867 Dec 27, 2022
A custom stretchable header view for UIScrollView or any its subclasses with UIActivityIndicatorView and iPhone X safe area support for content reloading. Built for iOS 10 and later.

Arale A custom stretchable header view for UIScrollView or any its subclasses with UIActivityIndicatorView support for reloading your content. Built f

Putra Z. 43 Feb 4, 2022
A framework which helps you attach observers to `UIView`s to get updates on its frame changes

FrameObserver is a framework that lets you attach observers to any UIView subclass and get notified when its size changes. It doesn't use any Method S

null 11 Jul 25, 2022
A way to quickly add a notification badge icon to any view. Make any view of a full-fledged animated notification center.

BadgeHub A way to quickly add a notification badge icon to any view. Demo/Example For demo: $ pod try BadgeHub To run the example project, clone the r

Jogendra 772 Dec 28, 2022
Simple and highly customizable iOS tag list view, in Swift.

TagListView Simple and highly customizable iOS tag list view, in Swift. Supports Storyboard, Auto Layout, and @IBDesignable. Usage The most convenient

Ela Workshop 2.5k Jan 5, 2023
Advanced List View for SwiftUI with pagination & different states

AdvancedList This package provides a wrapper view around the SwiftUI List view which adds pagination (through my ListPagination package) and an empty,

Chris 246 Jan 3, 2023
Full configurable spreadsheet view user interfaces for iOS applications. With this framework, you can easily create complex layouts like schedule, gantt chart or timetable as if you are using Excel.

kishikawakatsumi/SpreadsheetView has moved! It is being actively maintained at bannzai/SpreadsheetView. This fork was created when the project was mov

Kishikawa Katsumi 34 Sep 26, 2022
Fetch the star wars api from all the planets and list and show details using Swift UI and Combine

Star Wars Planets Fetch the star wars planet data by using stat war api, list and show details using SwiftUI and Combine frameworks ?? Swift UI Framew

null 1 Aug 10, 2022