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
Related
I've currently multiple items in tableView cell .I want to increase the height of cell when text condition is matched like if name = "john" then increase the height of cell without disturbing another cell. I want to achieve this screenshot result
My current code is
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == tableOrder {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.tableOrderHeight.constant = self.tableOrder.contentSize.height
//self.tableOrderHeight.constant = (6 * 80)
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PriceTVCell.self)) as? PriceTVCell else {return UITableViewCell()}
return cell
}
first get the index of the row you want to increase the height and use the delegate function heightForRowAt.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == customRow {
return 100.0;//Choose your custom row height
}
}
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()
So each of my tableview cells are full screen height similar to tiktok, igtv etc. When the user is scrolling I want the tableview to stop at each cell, not be able to scroll once and go past 2/3 cells.
I am using a custom tableview cell programmatically and this is how I am currently implementing the tableview delegate and datasource functions
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(PostViewTableCell.self, forCellReuseIdentifier: PostViewTableCell.cellReuseIdentifier)
tableView.allowsSelection = false
tableView.bounces = false
}
override var prefersStatusBarHidden: Bool {
return true
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel?.postInformations.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: PostViewTableCell.cellReuseIdentifier) as? PostViewTableCell else {
fatalError("Couldn't dequeue reusable cell")
}
cell.postViewCellOutput = self
cell.setUpCell(position: indexPath.row, viewModel: viewModel)
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.frame.size.height
}
So I am calling canPlay on the cell to play the cell that is fully visible on the screen so it can start playing. I am using this method to check visibility:
private func checkIfCellIsVisible(indexPath: IndexPath) -> Bool {
let cellRect = tableView.rectForRow(at: indexPath)
return tableView.bounds.contains(cellRect)
}
You can use UIScrollViewDelegate's func scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) on UITableViewDelegate since it is also a scroll view delegate to detect the end of drag and set the targetContentOffset to the row you want to, in this case the next row. For example:
var currIndexPath = IndexPath(row: 1, section: 0)
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let rect = tableview.rectForRow(at: self.currentIndexPath)
targetContentOffset.pointee = CGPoint(x: rect.minX, y: rect.minY)
self.currentIndexPath = IndexPath(row: self.currentIndexPath.row+1, section: 0)
}
The advantage here is that the height of your rows is full screen and the dragging is thus ensured to end in the current displayed row. This wouldn't work properly if the row height is not full screen as you would see the scroll happen past several cells but ultimately end up going to the cell you want.
Thanks for your response. Here is how I solved it. I set:
tableView.isScrollEnabled = false // Set the tableview scroll enabled to false
Then in the "cellForRowAt indexPath: IndexPath" function I saved the indexPath of the first row to the currentIndexPath property :
currentIndexPath = indexPath
Then I added a gesture recognizer to the tableview for swiping up:
let swipeToNextVideoGesture = UISwipeGestureRecognizer(target: self, action:
#selector(swipeToNext(_:)))
swipeToNextVideoGesture.direction = .up
tableView.addGestureRecognizer(swipeToNextVideoGesture)
Then in the function I first checked if the next Row is in the tableview datasource array bounds, then I scrolled to that row:
#objc private func swipeToNext(_ gesture: UISwipeGestureRecognizer) {
let nextVideoIndexPath = currentIndexPath.row+1
if !(nextVideoIndexPath >= viewModel.postInformations.count) {
currentIndexPath = IndexPath(row: nextVideoIndexPath, section: indexPathSection)
tableView.scrollToRow(at: currentIndexPath, at: .none, animated: true)
}
}
My tableview has a weird behavior when a cell is selected but this behavior is not seen always. When a cell has been selected some cells which are below the selected cell moves. These two gifs will show you the behavior in the two cases when it is shown and when it doesn't appear.
this is the
tableview without weird behavior and this is the tableview with the weird behavior
and this is my code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : UserTableViewCell = (tableView.dequeueReusableCell(withIdentifier: "userCell") as? UserTableViewCell)!
if(self.firebaseUsers.count != 0){
cell.influentBackgroudView.layer.cornerRadius=10
let url = URL.init(string: String().loadCorrectUrl(url:firebaseUsers[indexPath.row].image))
cell.influentImageView.contentMode = .scaleAspectFit
cell.influentImageView!.sd_setImage(with: url!, placeholderImage: UIImage.init(named: "placeholder"),options: [.continueInBackground,.progressiveDownload], completed: { (image, error, cacheType, imageURL) in
if (error != nil) {
cell.influentImageView!.image = UIImage(named: "placeholder")
} else {
cell.influentImageView.contentMode = .scaleAspectFill
cell.influentImageView.image = image
}
})
cell.influentImageView.layer.cornerRadius=10
cell.influentImageView.layer.masksToBounds = true
cell.influentNameLabel.text=" " + firebaseUsers[indexPath.row].name + " "
cell.influentNameLabel.adjustsFontSizeToFitWidth = true
cell.influentNameLabel.textAlignment = .center
if(selectedCellIndex==indexPath ){
cell.influentBackgroudView.isHidden=false
}
else{
cell.influentBackgroudView.isHidden=true
}
cell.selectionStyle = .none
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let previousIndex=selectedCellIndex
selectedCellIndex=indexPath
if(previousIndex==selectedCellIndex){
let nextVC = storyboard?.instantiateViewController(withIdentifier: "VisitedProfileViewController") as! VisitedProfileViewController
nextVC.passedUser = firebaseUsers[selectedCellIndex!.row]
navigationController?.pushViewController(nextVC, animated: true)
}
if(previousIndex==nil){
tableView.reloadRows(at: [indexPath], with:.none)
}
else{
if(previousIndex != indexPath){
tableView.reloadRows(at: [indexPath,previousIndex!], with: .none)
}
else {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
thank you guys for your help!
As identified from the comments the issue you are facing is produced by calling reloadRows when you press a cell with conjunction of incorrect estimated row heights. So either reloading needs to be removed or estimated height corrected. The first solution is already covered in an answer provided by A. Amini.
Since many of such anomalies are related to estimated row height it still makes sense to improve it.
For simple static row heights you can either implement a delegate method for instance
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return indexPath.row == 0 ? 44.0 : 200.0
}
where the values are exactly the same as in your heightForRowAt method. Or if all rows have same hight you can remove this delegate method but set the heights directly in some initialization method like:
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 200
tableView.estimatedRowHeight = tableView.rowHeight
}
When more complicated cells are introduced and automatic cell height is used we usually use a height cache of cells. It means we need to save height of a cell when it disappears so we may use it later. All heights are saved in a dictionary of type [IndexPath: CGFloat]. A very simple implementation should look like this:
private var cellHeightCache: [IndexPath: CGFloat] = [IndexPath: CGFloat]()
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightCache[indexPath] = cell.bounds.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeightCache[indexPath] ?? UITableView.automaticDimension
}
In some cases extra work is needed like clearing the cache when table view is reloaded.
The reason why this is happening is because table view will not reload cells around the cells you currently see but rather check the content offset and compute which cells you were supposed to be seeing. So any call to reload may make the table view jump or animate cells due to wrong estimated row height.
The main problem was you're reloading tableView after "each" selection.
Try this code out and let me know if you need more help.
class MyTableViewClass: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "userCell" as! UserTableViewCell
if !self.firebaseUsers.isEmpty {
cell.influentBackgroudView.layer.cornerRadius = 10
cell.influentImageView.contentMode = .scaleAspectFit
let url = URL(string: firebaseUsers[indexPath.row].image)
cell.influentImageView!.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"),options: [.continueInBackground,.progressiveDownload], completed: { (image, error, cacheType, imageURL) in
if error != nil {
cell.influentImageView.contentMode = .scaleAspectFill
}
})
cell.influentImageView.layer.cornerRadius=10
cell.influentImageView.layer.masksToBounds = true
cell.influentNameLabel.text = " \(firebaseUsers[indexPath.row].name) "
cell.influentNameLabel.adjustsFontSizeToFitWidth = true
cell.influentNameLabel.textAlignment = .center
cell.selectionStyle = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let nextVC = storyboard?.instantiateViewController(withIdentifier: "VisitedProfileViewController") as! VisitedProfileViewController
nextVC.passedUser = firebaseUsers[indexPath.row]
navigationController?.pushViewController(nextVC, animated: true)
}
}
class UserTableViewCell: UITableViewCell {
override var isSelected: Bool {
didSet {
if isSelected {
// Selected behavior, like change Background to blue
} else {
// Deselected behavior, change Background to clear
}
}
}
}
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