I'm using UITableView for showing banners which shows image and title.
https://youtu.be/4CnfZLWE3VI
The youtube link shows the animation of filtering banners. When I press "fav" button, it does not animate smoothly.
I'd like to have the animation smooth.
This is my swift code of UITableView.
extension HomeViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredTournamentlist.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "entryCell", for: indexPath) as? TournamentEntryCell {
cell.usernameLabel.text = filteredTournamentlist[indexPath.row].master_info.name
cell.titleTabel.text = self.filteredTournamentlist[indexPath.row].name
cell.dateLabel.text = DatetimeHelper.StringFromString(
string: self.filteredTournamentlist[indexPath.row].eventDate,
fromFormat: DatetimeHelper.DBDateFormat,
toFormat: DatetimeHelper.JPStringFormat
)
cell.gameInfoLabel.text = self.filteredTournamentlist[indexPath.row].gameName
self.tournamentModel.getThumbnail(path: self.filteredTournamentlist[indexPath.row].thumbnail_path) { image in
cell.thumbnailView.image = image
}
self.profileInfoModel.getIcon(path: self.filteredTournamentlist[indexPath.row].master_info.icon_path) { icon in
cell.iconView.image = icon
}
let selectedView = UIView()
selectedView.backgroundColor = Colors.plain
cell.selectedBackgroundView = selectedView
return cell
}
return UITableViewCell()
}
}
It just sets some information around banner. And this↓ is a code of collectionview which shows filter buttons such as "fav" and "plan".
extension HomeViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "buttonCell", for: indexPath) as! PlainSquareButtonCollectionViewCell
cell.button.setTitle(fileterButtonLabels[indexPath.row], for: .normal)
let input = HomeViewModel.Input(
getHomeTournamentsTrigger: Driver.never(),
getUserInfo: Driver.never(),
filter: cell.button.rx.tap
.map { [unowned self] in
return self.fileterButtonLabels[indexPath.row]
}
.asDriverOnErrorJustComplete(),
down: cell.rx.downButton
.map { b in
return b
}.asDriverOnErrorJustComplete()
)
let output = homeViewModel.transform(input: input)
output.filteredTournamentList
.drive(onNext: { [unowned self] tournamentList in
self.filteredTournamentlist = tournamentList
self.cardTableView.reloadData()
})
.disposed(by: disposeBag)
return cell
}
}
The tableView shows every information in filteredTournamentlist, and the content of filteredTournamentlist is changed by the filter buttons.
I don't know how to make the animation smooth...
Help me!
.drive(onNext: { [unowned self] tournamentList in
self.filteredTournamentlist = tournamentList
self.cardTableView.reloadData() // <<--------- RELOAD DATA
})
Calling reloadData() will not give you the expected animation. What ever changes are going to happen when you tap on fav button you need to first determine and then you can apply those changes inside beginUpdates() & endUpdates() block.
It seems you have single section in the table view showing items, if so then it will be simpler for me to explain how to achieve the table view reload animation.
Step 1: Determine the changes
Calculate number of rows before and after tap on fav or plan button from your data source.
Determine the IndexPaths which are going to be changed (reload, add, delete).
Step 2: Apply changes on table view with animation
Apply the change inside cardTableView.beginUpdates() & cardTableView.endUpdates().
Here is sample code for that
// ONLY CONSIDERING SINGLE SECTION, CALCULTION WILL BE COMPLEX FOR MULTI SECTION TABLEVIEW
// 1. Calculation of IndexPaths which are going to be impacted
let oldCellCount = dataSource.numberOfRowsInSection(0) // Get cell count before change
let newCellCount = dataSource.numberOfRowsInSection(0) // Get cell count after the change
var indexPathsToReload: [IndexPath] = []
var indexPathsToInsert: [IndexPath] = []
var indexPathsToDelete: [IndexPath] = []
if oldCellCount > newCellCount {
// Need to delete and reload few cells
indexPathsToReload = (0..<newCellCount).map { IndexPath(row: $0, section: 0) }
indexPathsToDelete = (newCellCount..<oldCellCount).map { IndexPath(row: $0, section: 0) }
} else if oldCellCount < newCellCount {
// Need to add and reload few cells
indexPathsToReload = (0..<oldCellCount).map { IndexPath(row: $0, section: 0) }
indexPathsToInsert = (oldCellCount..<newCellCount).map { IndexPath(row: $0, section: 0) }
} else {
// No change in cell count
indexPathsToReload = (0..<newCellCount).map { IndexPath(row: $0, section: 0)}
}
// 2. Reload with animation
tableView.beginUpdates()
tableView.deleteRows(at: indexPathsToDelete, with: .none) <<--- Use your expected animation here `fade`, `right`, `top`, `left` & more
tableView.reloadRows(at: indexPathsToReload, with: .none)
tableView.insertRows(at: indexPathsToInsert, with: .none)
tableView.endUpdates()
Related
I have tableView cells that are populated with a color in each cell. What I want is when the user taps on the cell, it "opens"/expands so that that color fills the entire screen. Currently, it only scales downwards from the cell that I click on. I also need it to scale upwards along the y-axis, each cell expanding to the top of the screen, but I'm not sure what's prohibiting it to.
let expandedColorView: UIView = {
let view = UIView()
return view
}()
#objc func userTap(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.ended {
let tapLocation = sender.location(in: self.paletteTableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {
UIView.animate(withDuration: 1.0, animations: {
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
} )
}
}
}
}
UITapGestureRecognizer is declared in tableView(cellForRowAt:) with cell.isUserInteractionEnabled = true enabled.
I've tried changing the expandedColorView bounds self.expandedColorView.bounds.size.height = UIScreen.main.bounds.height in UIView.animate but that doesn't change anything. I was thinking the cell's frame would need to change so that it matches the parent view frame (which I think would be tableView) but I couldn't figure out how to do that.
Any help would be appreciated!
I've attached a gif of the issue:
If that's what you want
This is what I have done in data source extension
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = self.tableView.dequeueReusableCell(withIdentifier: "colorCell") as? ColorFulTableViewCell {
let color = colors[Int(indexPath.row % 7)]
cell.backgroundColor = color
return cell
}
return UITableViewCell()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let row = expandCell?.row, row == indexPath.row {
return self.tableView.bounds.size.height
}
else {
return 100
}
}
}
And tableView delegate extension looks like
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadRows(at: [indexPath], with: .automatic)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Whats happening her?
In heightForRowAt of TableView data source method, I check if cell need to cover the whole tableView size by using if let row = expandCell?.row, row == indexPath.row { and set its height to match the tableView height by returning self.tableView.bounds.size.height else I return 100
In didSelectRowAt I update the indexPath of cell to expand by saving it in expandCell and I reload the row (so that this time when height for row is called it can return self.tableView.bounds.size.height and I also call scrollToRow(at with position as .top to ensure my cell scrolls to top and makes itself visible completely
Because you are reloading only a specific cell, though from cost perspective its efficient, but animation might look rusty as other cells in visible indexPath array are adjusting them selves abruptly, you can always call reload Data to get much better smoother experience.
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadData()
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Hope this helps
I have a UITableView that implements a type of 'infinite scrolling'.
This is done by calculating the IndexPath of the new data and then passing to tableView.insertRows(at: indexPaths, with: .none).
My data is returned from an api in pages of 50. What I am seeing however is when a user scrolls very quickly to the bottom, the table will insert rows and then jump to a section further up, essentially losing their place.
My table is created using this snippet
private func addTableView() {
tableView = UITableView(frame: .zero)
tableView.showsVerticalScrollIndicator = false
tableView.showsHorizontalScrollIndicator = false
tableView.rowHeight = UITableView.automaticDimension
tableView.bounces = true
tableView.estimatedRowHeight = 200
tableView.separatorStyle = .none
tableView.isHidden = true
tableView.backgroundColor = .clear
tableView.tableFooterView = UIView()
addSubview(tableView)
tableView.position(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.reuseID)
}
And the reload is triggered using
func insertRows(_ indexPaths: [IndexPath]) {
UIView.performWithoutAnimation {
tableView.insertRows(at: indexPaths, with: .none)
self.tableView.isHidden = false
}
}
I am using performWithoutAnimation as I did not like any of the animations for inserting rows.
In my view model I inject a FeedTableViewProvider conforming to UITableViewDataSource, UITableViewDelegate and has the following methods
protocol FeedTableViewProviderType: class {
var data: Feed? { get set }
var feed: [FeedItem] { get }
var insertRows: (([IndexPath]) -> Void)? { get set }
var didRequestMoreData: ((Int) -> Void)? { get set }
}
class FeedTableViewProvider: NSObject, FeedTableViewProviderType {
var insertRows: (([IndexPath]) -> Void)?
var didRequestMoreData: ((Int) -> Void)?
var data: Feed? {
didSet {
guard let data = data else { return }
self.addMoreRows(data.feed)
}
}
private(set) var feed = [FeedItem]() {
didSet {
isPaginating = false
}
}
private var isPaginating = false
private func addMoreRows(_ data: [FeedItem]) {
var indexPaths = [IndexPath]()
data.indices.forEach { indexPaths.append(IndexPath(row: feed.count + $0, section: 0)) }
feed.append(contentsOf: data.sorted(by: { $0.props.createdDate > $1.props.createdDate }))
insertRows?(indexPaths)
}
private func requestNextPage() {
guard let currentPage = data?.currentPage, let totalPages = data?.totalPages, currentPage < totalPages else { return }
didRequestMoreData?(currentPage + 1)
}
}
extension FeedTableViewProvider: TableViewProvider {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return feed.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.reuseID, for: indexPath)
cell.textLabel?.text = "Cell # \(indexPath.row)"
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.item == feed.count - 1 && !isPaginating {
isPaginating = true
requestNextPage()
}
}
}
I suspect the cause of this is actually to do with using
tableView.rowHeight = UITableView.automaticDimension
....
tableView.estimatedRowHeight = 200
The position changes as the offset is changing when new cells are inserted.
I would start by keeping some sort of cache containing your cell heights
private var sizeCache: [IndexPath: CGFloat] = [IndexPath: CGFloat]()
You can then capture that as the cell is scrolled off screen
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
sizeCache[indexPath] = cell.frame.size.height
}
Now make sure to apply that size from the cache
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return sizeCache[indexPath] ?? UITableView.automaticDimension
}
Now as cells are inserted and they jump with the new offset, they should render with their correct height, meaning the view should essentially stay on position.
use this code while adding new data into tableview.
UIView.setAnimationsEnabled(false)
self.tableView.beginUpdates()
self.tableView.reloadSections(NSIndexSet(index: 0) as IndexSet, with: UITableViewRowAnimation.none)
self.tableView.endUpdates()
You can use this function on every new row data insertion in Tableview to prevent scrolling to the bottom.
func scrollToTop(){
DispatchQueue.main.async {
let indexPath = IndexPath(row: self.dataArray.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
In table view
If use reloadData after adding a row, I think perfomence is not good
Is reloadData designed for that?
if not
How do I add a row?
FirstTime i Used reloadData()
now Used
let index = IndexPath(row: dataList.count - 1, section: 0)
tableView.storyTableView.insertRows(at: [index], with: .none)
but raise NSInternalInconsistencyException
So i will use reloadData again
Is there any problem if I keep using reloadData?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if dataList.count < 1{
self.emptyTableLabel.layer.zPosition = 2
}else{
self.emptyTableLabel.alpha = 0
}
return dataList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DataCell", for: indexPath) as! Datacell
cell.parentTable = self
let data = self.dataList[indexPath.row]
cell.indexLabel.text = "\(indexPath.row+1)"
cell.nick.text = (data["UserNick"] as! String)
cell.content.text = (data["Content"] as! String)
cell.index = indexPath.row
cell.time.text = (data["RegisterDate"] as! String).calculDateTime()
return cell
}
Dynamically adds values from the user to the table view,
It works well when data comes in one by one.
But An NSInternalInconsistencyException is thrown if multiple values are received almost simultaneously.
Go through this Perform updates method .
func performBatchUpdates(_ updates: (() -> Void)?,
completion: ((Bool) -> Void)? = nil)
Perform the insertion here and Reload the Tableview after Completion
tableView.performBatchUpdates ({
let index = IndexPath(row: dataList.count - 1, section: 0)
tableView.storyTableView.insertRows(at: [index], with: .none)
},
completion: { (success) in
tableView.reloadData()
}
)
Check this one
I am using UITableview inside tableView for one of my screen. Here I have one, InstalmentMainTableViewCell and InstalmentInnerTableViewCell.
I used below code to scroll inner tableView with full height:
class InnerTableView: UITableView {
override var intrinsicContentSize: CGSize {
//This is for extra space after inner tableview size. can be required
self.layoutIfNeeded()
return self.contentSize
}
}
Now, the problem is when I am scrolling from to second cell of MainTableViewCell from first it is getting stuck for a second and never happens again. It is only happening for first time whenever the view-controller appears.
Here is the full code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch (tableView.tag) {
case 100:
return instalmentModel.count == 0 ? 0 : instalmentModel.count
default:
return instalmentModel[currentInstalmentIndex].EMIDetailModel.count == 0 ? 0 : instalmentModel[currentInstalmentIndex].EMIDetailModel.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch (tableView.tag) {
case 100:
currentInstalmentIndex = indexPath.row
let cell = tableView.dequeueReusableCell(withIdentifier: InstalmentsMainTableViewCell.className) as! InstalmentsMainTableViewCell
if let model = self.instalmentModel[indexPath.row] as InstalmentModel? {
if tableView.visibleCells.contains(cell) {
self.putValue(self.yearLabel, "\(String(describing: model.year!))")
}
cell.emiTotal,text = "\(model.year!)"
}
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: InstalmentsInnerTableViewCell.className) as! InstalmentsInnerTableViewCell
if let model = self.instalmentModel[self.currentInstalmentIndex].EMIDetailModel[indexPath.row] as EMIDetailModel? {
cell.indicatorView.backgroundColor = UIColor.red
}
return cell
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let indexPath = self.instalmentsTableView.indexPathsForVisibleRows?[0]
if let model = instalmentModel[(indexPath?.row)!] as InstalmentModel? {
putValue(yearLabel, "\(model.year!)")
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.view.layoutIfNeeded()
self.view.setNeedsLayout()
}
The requirement is:
Requirement for tableView inside tableview
I want to make a tableview with a button in the section. I want the button to add one more row to a tableview like this
Here is the source code:
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection sectionInd: Int) -> Int {
if sectionInd == 0 {
return others.count
} else {
return 1
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "ShareCell", for: indexPath as IndexPath) as! SelectOthersTableViewCell
cell.firstName.text = others[indexPath.row].firstname
cell.lastName.text = others[indexPath.row].lastname
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "addCell", for: indexPath as IndexPath) as! addTableCell
cell.addCells.tag = indexPath.row
cell.addCells.addTarget(self, action: #selector(OthersViewController.addButtonClicked(sender:)), for: UIControlEvents.touchUpInside)
return cell
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var height:CGFloat = CGFloat()
if indexPath.section == 0 {
height = 145
} else {
height = 50
}
return height
}
#objc func addButtonClicked(sender:UIButton) {
data.append("Guest 1")
let buttonPosition = sender.convert(CGPoint.zero, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: buttonPosition)
print("indexPath \(indexPath!)")
selectedIndexes[indexPath!] = !(selectedIndexes[indexPath!] ?? false)
tableView.reloadRows(at: [indexPath!], with: .automatic)
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: data.count-1, section: 0)], with: .automatic)
tableView.endUpdates()
}
i need help please. How to add new row by tap button on icon (+)?
On click of "Add" button, You should not reload the the entire table view because it increases the processing time. Instead of that you can use of
beginUpdates and endUpdates for inserting new cell when button clicked.
Basic Approaches:
(1). On click of "Add", update your data-source for table-view.
dataSource.append(NewRecord)
(2). Insert the new cell:
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: dataSource.count-1, section: 0)], with: .automatic)
tableView.endUpdates()
Reviewing your Code:
func addButtonClicked(sender:UIButton) {
data.append("Guest 1")
.....
}
Your datasource is others on which the tableview is created and configured.
But on click of add button (addButtonClicked function), you are not updating the others data-source. Please verify it, except that your code seems good.
fun onPlusButtonClicked(){
sections.append(whatever you want)
items[2].append(["1", "2", "3", "4"]) // whatever you want to add here
tableview.reloadData() // you can call this on a background thread as well, if its not working
}
// Ex of how to use with tableview
var sections = Your array
var items = your array
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section]
}
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items[section].count
}