SkillAgentSearch skills...

LiveCollections

Automatically perform UITableView and UICollectionView animations between two sets of immutable data. It supports generic data types and is fully thread-safe.

Install / Use

/learn @scribd/LiveCollections

README

<img src="https://github.com/scribd/LiveCollections/blob/master/ReadMe/LiveCollections_main_logo.png" alt="LiveCollections logo"> <img src="https://github.com/scribd/LiveCollections/blob/master/ReadMe/LiveCollections_Animated.png" alt="Single section collection view class graph">

<b>LiveCollections</b> is an open source framework that makes using UITableView and UICollectionView animations possible in just a few lines of code. Given two sets of data, the framework will automatically perform all of the calculations, build the line item animation code, and perform it in the view.

Using one of the two main classes CollectionData or CollectionSectionData, you can build a fully generic, immutable data set that is thread safe, timing safe, and highly performant. Simply connect your view to the data object, call the update method and that's it.

In the sample app, there are a number of use case scenarios demonstrated, and the sample code of each one can be found by looking up the respective controller (e.g. ScenarioThreeViewController.swift).

Full detail for the use case of each scenario <a href="https://medium.com/p/59ea1eda2b2d">can be found in the blog post on Medium</a>, which you can read if you want the full explanation. Below, I am just going to show the class graph and the minimum code needed for each case.

<hr> <h2>Swift Version</h2>

This project has been upgraded to be compatible with Swift 5.5 <br>

<h2>iOS Version</h2>

This minimum supported version is iOS 11.0 <br>

<hr> <h2>Importing With SwiftPM</h2> <br> https://github.com/scribd/LiveCollections 1.0.3 <br> <br> <h2>Importing With Carthage</h2> <br> github "scribd/LiveCollections" "1.0.3" <br> <br> <h2>Importing With CocoaPods</h2> <br> pod 'LiveCollections', '~> 1.0.3' <br> or <br> pod 'LiveCollections' <br> <br> <hr> <h1>The Main Classes</h1> <ul> <li><b>CollectionData&lt;YourType&gt;</b></li> <li><b>CollectionSectionData&lt;YourSectionType&gt;</b></li> </ul>

By using one of these two classes to hold your data, every time you update with new data it will automatically calculate the delta between your update and the current data set. If you assign a view to either object, then it will pass both the delta and data to the view and perform the animation automatically. All updates are queued, performant, thread safe, and timing safe. In your UITableViewDataSource and UICollectionViewDataSource methods, you simply need to fetch your data from the <b>CollectionData</b> or <b>CollectionSectionData</b> object directly using the supplied count, subscript, and isEmpty thread-safe accessors.

To prepare your data to be used in <b>CollectionData</b>, you just need to adopt the protocol <b>UniquelyIdentifiable</b>. The requirements for <b>CollectionSectionData</b> will be detailed in scenerios 5 and 6 a bit later on.

<BR>

<b>Updates made easy!</b>

Once you create an instance of CollectionData, animating your table or collection view becomes just a single line of code:

func yourDataUpdateFunction(_ updatedData: [YourDataType]) {
    collectionData.update(updatedData)
}

<BR>

You'll notice that the data being passed in is a Swift immutable type, and at no point do you ever need to worry about what the difference is between the new data being passed in and the existing data.

<BR> <h2>Adopting the protocol UniquelyIdentifiable</h2>

The crux of being able to use <b>CollectionData</b> as your data source and get all of the benefits of LiveCollections, is by adopting the protocol <b>UniquelyIdentifiable</b>. It's what allows the private delta calculator in the framework to determine all of the positional moves of your data objects.

public protocol UniquelyIdentifiable: Equatable {
    associatedtype RawType
    associatedtype UniqueIDType: Hashable
    var rawData: RawType { get }
    var uniqueID: UniqueIDType { get }
}

Since UniquelyIdentifiable inherits from Equatable, making your base class adopt Equatable gives you an auto-synthesized equatability function (or you can write a custom == func if needed).

Here's a simple example of how it can apply to a custom data class:

import LiveCollections

struct Movie: Equatable {
    let id: UInt
    let title: String
}

extension Movie: UniquelyIdentifiable {
    typealias RawType = Movie
    var uniqueID: UInt { return id }
}

Note: Take a look at Scenario 9 below to see an example where RawType is not simply the Self type. We will use a different type if we want to have different equatability functions for the same RawType object in different views, or if we want to create a new object that includes additional metadata.

<BR> <h2>Adopting the protocol NonUniquelyIdentifiable</h2>

Support has been added for non-unique sets of data as well.

public protocol NonUniquelyIdentifiable: Equatable {
    associatedtype NonUniqueIDType: Hashable
    var nonUniqueID: NonUniqueIDType { get }
}

By adopting this protocol and using one of the two type aliases NonUniqueCollectionData or NonUniqueCollectionSectionData, a factory will be built under the hood that will transform your non-unique data into a UniquelyIdentifiable type. See Scenarios 10 and 11.

Since the data is wrapped in a new struct, to access your original object you'll need to call the rawData getter like so:

let data = collectionData[indexPath.item].rawData

<i>Note:This will use "best guess" logic, and the identifiers will be determined based on array order.</i> <br> <br>

<hr>

Listed below is a summation of the relevant code you'll need in your app for each of the scneario in the sample app. These reflect most of the use cases you will encounter. <br>

<h2>Scenario 1: A UICollectionView with one section</h2> <img src="https://github.com/scribd/LiveCollections/blob/master/ReadMe/single_section_collection_view.png" alt="Single section collection view class graph">
final class YourClass {
    private let collectionView: UICollectionView
    private let collectionData: CollectionData<YourData>

    init(_ data: [YourData]) {
        collectionData = CollectionData(data)
        ...
        super.init()
        collectionData.view = collectionView
    }

    func someMethodToUpdateYourData(_ data: [YourData]) {
        collectionData.update(data)
    }
}

extension YourClass: UICollectionViewDelegate {
  
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return collectionData.count
    }  
    
    // etc
}
<br> <br> <br> <h2>Scenario 2: A UTableView with one section</h2> <br> The same as scenario 1 but swap in UITableView. <br> <br> <br> <h2>Scenario 3: A UICollectionView with multiple sections, each section has its own CollectionData</h2> <img src="https://github.com/scribd/LiveCollections/blob/master/ReadMe/discrete_sections_collection_view.png" alt="Multiple discrete sections collection view class graph">

or

<img src="https://github.com/scribd/LiveCollections/blob/master/ReadMe/discrete_sections_collection_view_with_synchronizer.png" alt="Multiple discrete sections collection view class graph with two sections synchronized">
final class YourClass {
    private let collectionView: UICollectionView
    private let dataList: [CollectionData<YourData>]

    init() {
        // you can also assign section later on if that better fits your class design
        dataList = [
             CollectionData<YourData>(section: 0),
             CollectionData<YourData>(section: 1),
             CollectionData<YourData>(section: 2)
        ]
        ...
        super.init()
        
        // Optionally apply a synchronizer to multiple sections to have them
        // perform their animations in the same block when possible
        let synchronizer = CollectionDataSynchronizer(delay: .short)
        dataList.forEach { $0.synchronizer = synchronizer }
    }

    func someMethodToUpdateYourData(_ data: [YourData], section: Int) {
        let collectionData = dataList[section]
        collectionData.update(data)
    }
}

extension YourClass: UICollectionViewDelegate {
  
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return dataList.count
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        let collectionData = dataList[section]
        return collectionData.count
    }  
    
    // let item = collectionData[indexPath.row]
    // etc
}
<br> <br> <br> <h2>Scenario 4: A UITableView with multiple sections, each section has its own CollectionData</h2> <br> The same as scenario 3 but swap in UITableView. <br> <br> <hr> <h1>Using unique data across multiple sections? Adopt the protocol UniquelyIdentifiableSection</h1>

When data items are uniquely represented across the entire view, they may move between sections. To handle these animations, you can instead use <b>CollectionSectionData</b> and create a data item that adopts <b>UniquelyIdentifiableSection</b>.

public protocol UniquelyIdentifiableSection: UniquelyIdentifiable {
    associatedtype DataType: UniquelyIdentifiable
    var items: [DataType] { get }
}

As you can see, it still ultiately relies on the base data type that adopts <b>UniquelyIdentifiable</b>. This new object helps us wrap the section changes.

Note: Since UniquelyIdentifiableSection inherits from UniquelyIdentifiable, that means that each section will also require its own uniqueID to track section changes. These IDs do not have to be unique from those of the underlying items: [DataType].

import LiveCollections

struct MovieSection: Equatable {
    let sectionIdentifier: String
    let movies: [Movie]
}

extension MovieSection: UniquelyIdentifiableSection {
    var uniqueID: String { return sectionIdentifier }
    var items: [Movie] { return movies }
    var hashValue: Int { return items.reduce(uniqueID.hashValue) { $0 

Related Skills

View on GitHub
GitHub Stars342
CategoryCustomer
Updated14d ago
Forks14

Languages

Swift

Security Score

100/100

Audited on Mar 20, 2026

No findings