轻量级前端框架助力开发者提升项目效率与性能
637
2022-11-05
LiveCollections是一个开源框架,只需几行代码就可以使用UITableView和UICollectionView动画
LiveCollections 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 can be found in the blog post on Medium, 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.
Swift Version
This project has been upgraded to be compatible with Swift 5.
Importing With Carthage
github "scribd/LiveCollections" "beta_0.9.12"
Importing With CocoaPods
pod 'LiveCollections', '~> 0.9.12'
or
pod 'LiveCollections'
The Main Classes
CollectionData
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 CollectionData or CollectionSectionData object directly using the supplied count, subscript, and isEmpty thread-safe accessors.
To prepare your data to be used in CollectionData, you just need to adopt the protocol UniquelyIdentifiable. The requirements for CollectionSectionData will be detailed in scenerios 5 and 6 a bit later on.
Updates made easy!
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)}
Adopting the protocol UniquelyIdentifiable
The crux of being able to use CollectionData as your data source and get all of the benefits of LiveCollections, is by adopting the protocol UniquelyIdentifiable. 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 LiveCollectionsstruct 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.
Adopting the protocol NonUniquelyIdentifiable
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
Note:This will use "best guess" logic, and the identifiers will be determined based on array order.
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.
Scenario 1: A UICollectionView with one section
final class YourClass { private let collectionView: UICollectionView private let collectionData: CollectionData
Scenario 2: A UTableView with one section
The same as scenario 1 but swap in UITableView.
Scenario 3: A UICollectionView with multiple sections, each section has its own CollectionData
or
final class YourClass { private let collectionView: UICollectionView private let dataList: [CollectionData
Scenario 4: A UITableView with multiple sections, each section has its own CollectionData
The same as scenario 3 but swap in UITableView.
Using unique data across multiple sections? Adopt the protocol UniquelyIdentifiableSection
When data items are uniquely represented across the entire view, they may move between sections. To handle these animations, you can instead use CollectionSectionData and create a data item that adopts UniquelyIdentifiableSection.
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 UniquelyIdentifiable. 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 LiveCollectionsstruct 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 ^ $1.hashValue } }}
Scenario 5: A UICollectionView with multiple sections and a singular data source
final class YourClass { private let collectionView: UICollectionView private let collectionData: CollectionSectionData
Scenario 6: A UITableView with multiple sections and a singular data source
The same as scenario 5 but swap in UITableView (I bet you didn't see that coming).
Scenario 7: A Table of Carousels
Table view data source
final class YourClass { private let tableView: UITableView private let collectionData: CollectionData
A table view cell to contain the collection view
final class CarouselTableViewCell: UITableViewCell { private let collectionView: UICollectionView ...}
Carousel data source
final class SomeCarouselDataSource: UICollectionViewDelegate { private let collectionView: UICollectionView private let collectionData: CollectionData
Scenario 8: A Sectioned Table of Carousels (carousels can move between sections)
Almost everything is the same as the previous example, except that our table view uses CollectionSectionData.
final class YourClass { private let tableView: UITableView private let collectionData: CollectionSectionData
Scenario 9: Using a Data Factory
A data factory you build must conform to the protocol UniquelyIdentifiableDataFactory. It's role is to simply take in a value of RawType and build a new object. This can be as simple as a wrapper class that modifies the equatability function, or could be a complex factory that injects multiple data services that fetch metadata for the RawType item.
What using a data factory means is that your update method on CollectionData takes in [RawType], but returns a value of [UniquelyIdentifiableType] (your new data class) when requesting values via subscript. This also saves your data source from needing to know about any custom types you are building to customize your view.
The buildQueue property will default to nil via an extension, and is only needed if your data needs to be build on a specific thread. Otherwise ignore it.
public protocol UniquelyIdentifiableDataFactory { associatedtype RawType associatedtype UniquelyIdentifiableType: UniquelyIdentifiable var buildQueue: DispatchQueue? { get } // optional queue if your data is thread sensitive func buildUniquelyIdentifiableDatum(_ rawType: RawType) -> UniquelyIdentifiableType}
Here is the example I use in the sample app. It takes in an injected controller that looks up whether a movie is currently playing in theaters, and creates a new object that includes this data. The equatability function includes this metadata, and thus changes the conditions of what constitutes a reload action in the view animation.
import LiveCollectionsstruct DistributedMovie: Hashable { let movie: Movie let isInTheaters: Bool}extension DistributedMovie: UniquelyIdentifiable { var rawData: Movie { return movie } var uniqueID: UInt { return movie.uniqueID }}struct DistributedMovieFactory: UniquelyIdentifiableDataFactory { private let inTheatersController: InTheatersStateInterface init(inTheatersController: InTheatersStateInterface) { self.inTheatersController = inTheatersController } func buildUniquelyIdentifiableDatum(_ movie: Movie) -> DistributedMovie { let isInTheaters = inTheatersController.isMovieInTheaters(movie) return DistributedMovie(movie: movie, isInTheaters: isInTheaters) }}
Once you build your factory, the only real change to your code is injecting it into the initializer:
final class YourClass { private let collectionView: UICollectionView private let collectionData: CollectionData
Scenario 10: Non-unique data in a single section
Use the typealiased data struct NonUniqueCollectionData with your non-unique data.
final class YourClass { private let collectionView: UICollectionView private let collectionData: NonUniqueCollectionData
Scenario 11: Non-unique data in multiple sections
Use the typealiased data struct NonUniqueCollectionSectionData with your non-unique section data.
final class YourClass { private let collectionView: UICollectionView private let collectionData: NonUniqueCollectionSectionData
Scenario 12: Manual timing of the animation
In every previous case we have assigned the view object to the CollectionData object. If you choose to omit this step, you can still get the benefits of LiveCollections caltulations.
Simply do the following:
let delta = collectionData.calculateDelta(data)// perform any analysis or analytics on the deltalet updateData = { self.collectionData.update(data)}// when the time is right, call...collectionView.performAnimations(section: collectionData.section, delta: delta, updateData: updateData)
Note: This is unavailable for CollectionSectionData as the animations occur in multiple steps and the timing of the updates is very specific.
Scenario 13: Custom table view animations
By default LiveCollections performs a preset selection of table view animations: delete (.bottom), insert (.fade), reload (.fade), reloadSection (.none).
As of 0.9.8 there is support for overriding these defaults and setting your own values.
There is a new accessor on CollectionData to set up your view instead of using collectionData.view = tableView.
let rowAnimations = TableViewAnimationModel(deleteAnimation: .right, insertAnimation: .right, reloadAnimation: .middle)collectionData.setTableView(tableView, rowAnimations: rowAnimations, sectionReloadAnimation: -)
Scenario 14: Multiple data sources pointing at the same table view with custom animations
If you have multiple data souces each animating a section of a table view, you can give each of them custom animations. They can even be different per section if that's what you really want.
let sectionZeroRowAnimations = TableViewAnimationModel(deleteAnimation: .left, insertAnimation: .left, reloadAnimation: .left)dataList[0].setTableView(tableView, rowAnimations: sectionZeroRowAnimations)let sectionOneRowAnimations = TableViewAnimationModel(deleteAnimation: .right, insertAnimation: .right, reloadAnimation: .right)dataList[1].setTableView(tableView, rowAnimations: sectionOneRowAnimations)let sectionTwoRowAnimations = TableViewAnimationModel(deleteAnimation: -, insertAnimation: -, reloadAnimation: -)dataList[2].setTableView(tableView, rowAnimations: sectionTwoRowAnimations)
Scenario 15: Custom table view animations for data across all sections
CollectionSectionData has a new initializer.
let rowAnimations = TableViewAnimationModel(deleteAnimation: .right, insertAnimation: .right, reloadAnimation: .right)let sectionAnimations = TableViewAnimationModel(deleteAnimation: .left, insertAnimation: .left, reloadAnimation: .left)return CollectionSectionData
I hope this covers nearly all of the use cases out there, but if you find a gap in what this framework offers, I'd love to hear your suggestions and feedback.
Happy animating!
Special thanks to The Movie Database. All of the images and data in the sample application were retrieved from their open source API. It's an excellent tool that helped save a lot of time and hassle. Using their data, the examples demonstrate how you can take existing data and extend it to use LiveCollections.
This product uses the TMDb API but is not endorsed or certified by TMDb.
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~