Imagine the screen of a regular mobile application with an already filled list of cells. From the server comes another list. It is necessary to calculate the difference between them (what was added / deleted) and to do a UICollectionView
.
The “simple” approach is to completely replace the model and then call reloadData
. Unfortunately, animations are lost and other unwanted effects and brakes can occur. Much more interesting to edit lists neatly, animated. After trying to do this several times, I made sure that it is incredibly difficult.
Once the problem has been encountered in several projects, it is necessary to generalize it and work further with a generalized implementation. An interesting challenge! A few days of dealing with the documentation, common sense, bugs implementation tables in iOS, and it turned out the code with a fairly simple interface, adapting to a wide range of tasks, about which I want to tell.
Of course, the framework first solves my own problems. If you suddenly need a feature that is not there now, write, ask, I will try to refine.
Imagine that we have a table that consists of sections with cells.
Tables or lists areUICollectionView
orUITableView
, I will not distinguish them in the article. Judging by the same bugs, inside there is the same code, and the interface is similar.
You need to be able to animate the table in two cases:
If something clear has changed (for example, one cell has been added), then everything is simple. But what if we have a chat in which messages can be edited and deleted in batches? Or a list of users that is shown from the cache, and then it is obtained from the server and is completely updated?
For example, try to present the address book, where the sorting was from A to Z, and then it changed the reverse. The last sections should move up, and within sections the cells should be re-sorted. What are the indices of displacements? In what sequence will the system apply animations? All these questions are very superficially described in the documentation, and you have to understand the method of "spear".
ATableAnimationCalculator
is a data model for a table that monitors the current state of the cells and, if it is told “something new here, calculate the difference”, considers counting out the list of cell indexes and sections that need to be changed (deleted, inserted, moved). After that, the result of the calculation can be applied to the table, bypassing problems in the implementation of iOS animations.
In the names, the first letter “A” is not a framework prefix, as you might think, but an abbreviation of the word “Awesome”. ;-)
The framework consists of:
ACellModel
protocol to be implemented in the cell model.ASectionModel
(and ASectionModelObjC
to support Objctive-C), from which you need to inherit the section model. A class, not a protocol, in order not to repeat the code dedicated to the internal device of the sections.ACellSectionModel
protocol, whose implementation knows how to link cells and sections.ATableAnimationCalculator
.ATableDiff
(with extensions for UIKit'a that live in a separate file).Section class is quite simple. It is needed to store the start / end indexes, but since these are implementation details, only the initializer and indexes stick out, which can be useful for debugging purposes. The ASectionModelObjC
class ASectionModelObjC
exactly the same; it should be used when Objective-C support is required.
public class ASectionModel: ASectionModelProtocol { public internal (set) var startIndex:Int public internal (set) var endIndex:Int public init() }
Cell protocol is no more complicated. It is necessary that the cells are equal, you need to check their contents for identity and to be able to copy them (why in the section on rakes).
public protocol ACellModel: Equatable { // init(copy:Self) // , , func contentIsSameAsIn(another:Self) -> Bool }
There is also a protocol linking cells and sections together. It helps to understand whether two cells are in one section and create a section in an arbitrary cell. Please note that the ASectionModel
section type must be inherited from the ASectionModel
class and implement the Equatable
protocol.
public protocol ACellSectionModel { associatedtype ACellModelType: ACellModel associatedtype ASectionModelType: ASectionModelProtocol, Equatable // , , , // func cellsHaveSameSection(one one:ACellModelType, another:ACellModelType) -> Bool // func createSection(forCell cell:ACellModelType) -> ASectionModelType }
The ATableAnimationCalculator
class has a comparator that is used to sort the cells, several methods for use in the .dataSource
tables, and methods for starting the calculation of changes. Also for debugging it may be useful to look at the lists of cells and sections.
public class ATableAnimationCalculator<ACellSectionModelType:ACellSectionModel>: NSObject { // , , private typealias ACellModelType = ACellSectionModelType.ACellModelType private typealias ASectionModelType = ACellSectionModelType.ASectionModelType // public private(set) var items:[ACellModelType] public private(set) var sections:[ASectionModelType] // . // resortItems public var cellModelComparator:(ACellModelType, ACellModelType) public init(cellSectionModel:ACellSectionModelType) } public extension ATableAnimationCalculator { // ( ) // .dataSource .delegate func sectionsCount() -> Int func itemsCount(inSection sectionIndex:Int) -> Int func section(withIndex sectionIndex:Int) -> ACellModelType.ASectionModelType func item(forIndexPath indexPath:NSIndexPath) -> ACellModelType func item(withIndex index:Int) -> ACellModelType } public extension ATableAnimationCalculator { // diff, // (, , ) func resortItems() throws -> DataSourceDiff // , . // reloadData, . func setItems(newItems:[ACellModelType]) throws -> DataSourceDiff // , . func updateItems(addOrUpdate addedOrUpdatedItems:[ACellModelType], delete:[ACellModelType]) throws -> DataSourceDiff }
The calculator is specially made as independent as possible so that you can use it anywhere. For UICollectionView
and UITableView
, the corresponding extensions are written that allow you to animate the results of the calculations to them:
public extension ATableDiff { func applyTo(collectionView collectionView:UICollectionView) func applyTo(tableView tableView:UITableView) }
Let's look at a simple implementation of a section in which there is only a header.
public class ASectionModelExample: ASectionModel, Equatable { public let title:String public init(title:String) { self.title = title super.init() } } public func ==(lhs:ASectionModelExample, rhs:ASectionModelExample) -> Bool { return lhs.title == rhs.title }
There are three fields in the cell:
class ACellModelExample: ACellModel { var id:String var header:String var text:String init(text:String, header:String) { id = NSUUID().UUIDString // self.text = text self.header = header } required init(copy:ACellModelExample) { id = copy.id text = copy.text header = copy.header } func contentIsSameAsIn(another:ACellModelExample) -> Bool { return text == another.text } } func ==(lhs:ACellModelExample, rhs:ACellModelExample) -> Bool { return lhs.id == rhs.id }
And finally, a class that knows how to link cells and sections together.
class ACellSectionModelExample: ACellSectionModel { func cellsHaveSameSection(one one:ACellModelExample, another:ACellModelExample) -> Bool { return one.header == another.header } func createSection(forCell cell:ACellModelExample) -> ASectionModelExample { return ASectionModelExample(title:cell.header) } }
Now let's see how to screw this all to UITableView
. First, we connect the calculator to the table's .dataSource'
methods. This is easy to do, since the calculator assumes all requests for the number and receipt of items by index.
The code is intentionally made minimal in size, the real code must be more accurate and, please, without exclamation marks. :-)
// private let calculator = ATableAnimationCalculator(cellSectionModel:ACellSectionModelExample()) func numberOfSectionsInTableView(tableView:UITableView) -> Int { return calculator.sectionsCount() } func tableView(tableView:UITableView, numberOfRowsInSection section:Int) -> Int { return calculator.itemsCount(inSection:section) } func tableView(tableView:UITableView, cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("generalCell") cell!.textLabel!.text = calculator.item(forIndexPath:indexPath).text return cell! } func tableView(tableView:UITableView, titleForHeaderInSection section:Int) -> String? { return calculator.section(withIndex:section).title }
The first update of the data usually does not need to be animated, so just set the list and call, as usual, reloadData
. The calculator will sort (if a comparator is set) the cells and break them into sections.
try! calculator.setItems([ ACellModelExample(text:"5", header:"C"), ACellModelExample(text:"1", header:"A"), ACellModelExample(text:"3", header:"B"), ACellModelExample(text:"2", header:"B"), ACellModelExample(text:"4", header:"C") ]) tableView.reloadData()
The update may break if the comparator is not set, and the cells themselves are not sorted in advance. After all, then it may turn out that the same section at the same time will be scattered in different parts of the list, which is difficult to analyze. :-)
Now, for example, add a couple of cells in different sections and apply the calculated animations.
let addedItems = [ ACellModelExample(text:"2.5", header:"B"), ACellModelExample(text:"4.5", header:"C"), ] let itemsToAnimate = try! calculator.updateItems(addOrUpdate:addedItems, delete:[]) itemsToAnimate.applyTo(tableView:tableView)
You can also change the comparator, and then animate re-sort the cells.
calculator.cellModelComparator = { left, right in return left.header < right.header ? true : left.header > right.header ? false : left.text < right.text } let itemsToAnimate = try! self.calculator.resortItems() itemsToAnimate.applyTo(tableView:self.tableView)
Actually, everything.
Remember the copy constructor in the cell model? In it, you need to copy the entire cell, and the ID (from the example), and these cells (header, text in the example). Otherwise, it may happen that when the data in the model changes, they will also change inside the algorithm data. After that, the algorithm will not be able to determine that the cells have been updated. There will be implicit bugs with non-updating cells that are hard to understand.
Another field of the rake hides the algorithm - a complex update of the tables and bugs of the current implementation of iOS. For example, now in the case of simultaneous movement of sections and cells within these sections, it does not work out the update of cells, it is necessary to ask them to force them. You need to remember this if you decide not to use the already written methods, but to implement them yourself.
During testing, I found out that the performBatchUpdates
method works, let's say, strange. In the simulator, he can give, for example, EXC_I386_DIV
(exclusion of division by zero). Sometimes it happens that asserts work (about which nothing is unknown, only the line number in the depths of UIKit). If suddenly you have cases when everything breaks down and they stably repeat - write, I will try to embed code that will take them into account.
You can try to use the calculator in code for Objective-C. This is not very convenient, and I did not set a goal to support Objective-C, but it is possible. This is done like this:
@objc class ACellModelExampleObjC: NSObject, ACellModel
,@objc public class ASectionModelExampleObjC: ASectionModelObjC
(the base class is important here).class ACellSectionModelExample ObjC: ACellSectionModel
@objc class ATableAnimationCalculatorObjC: NSObject { private let calculator = ATableAnimationCalculator(cellSectionModel:ACellSectionModelExampleObjC()) func getCalculator() -> AnyObject? { return calculator } func setItems(items:[ACellModelExampleObjC], andApplyToTableView tableView:UITableView) { try! calculator.setItems(items).applyTo(tableView:tableView) } }
Then you can use it in Objective-C.
#import "ATableAnimationCalculator-Swift.h" ATableAnimationCalculatorObjC *calculator = [[ATableAnimationCalculatorObjC alloc] init]; [calculator setItems:@[ [[ACellModelExampleObjC alloc] initWithText:@"1" header:@"A"], [[ACellModelExampleObjC alloc] initWithText:@"2" header:@"B"], [[ACellModelExampleObjC alloc] initWithText:@"3" header:@"B"], [[ACellModelExampleObjC alloc] initWithText:@"4" header:@"C"], [[ACellModelExampleObjC alloc] initWithText:@"5" header:@"C"], ] andApplyToTableView:myTableView];
As you can see, in Swift, you will need to render all the work with the ATableDiff
structure, and the calculator itself will be output in Objective-C as id
( AnyObject?
).
The comments suggested adding methods to support standard UITableView editing. Added by. You can use it like this:
let itemsToAnimate = try! calculator.removeItem(withIndex:indexPath) // let itemsToAnimate = try! calculator.swapItems(withIndex:sourceIndexPath, toIndex:destinationIndexPath) //
And the addition is clear. In this case, you only need to take into account that if you are going to work with moving cells, you need to “turn off” the cellModelComparator
, or check that it takes into account the correct position of the cells when moving.
The code is tested on a bunch of artificial / random tests. As far as I can see, it works quite well. If you see any shortcomings or unrecorded cases, write.
Using generics and associated types breaks (judging by the responses to StackOverflow) compatibility with iOS 7, therefore only iOS 8 and 9 are supported.
Sources live on GitHub, the project is called ATableAnimationCalculator
. For integration, you can include source code to yourself (there are only a few files). If you only need an algorithm, you can connect everything except extensions for UIKit.
There are under in CocoaPods :
pod 'AwesomeTableAnimationCalculator'
Supported by Carthage :
github "bealex/AwesomeTableAnimationCalculator"
If you have any questions, ask either here or directly to the email alex@jdnevnik.com .
Thanks to Evgeny Egorov for the improvements in the structure and additional test cases that made it possible to improve the algorithm.
Source: https://habr.com/ru/post/282509/
All Articles