I need to expand or collapse a table view cell and its contents.
For that I'm using NSLayoutConstraints. Though it gets the work done, I'm facing some pesky layout issues with other views in the cell.Here is a video.And here is my code:
ViewController:
extension ViewController: TableViewCellDelegate {
func tableView(shouldExpand cell: TableViewCell) {
tableView.performBatchUpdates({
cell.expand()
})
}
func tableView(shouldCollapse cell: TableViewCell) {
tableView.performBatchUpdates({
cell.collapse()
})
}
}
TableViewCell:
func expand() {
UIView.animate(withDuration: 0.57) {
self.viewHeight.constant = 320
self.view.alpha = 1
self.layoutIfNeeded()
}
}
func collapse() {
UIView.animate(withDuration: 0.57) {
self.viewHeight.constant = 0
self.view.alpha = 0
self.layoutIfNeeded()
}
}
How to stop the upper view stop resizing?
You can use UIStackView and just show and hide view for collapse and expand.
UIStackView will be easy to handle and will work for you.
performBatchUpdates is used to animate changes in tableView based on changes in the data source. Since you don't have any change in datasource, there is no need to do the animation inside tableView.performBatchUpdates block. Try it without performBatchUpdates.
func tableView(shouldExpand cell: TableViewCell) {
cell.expand()
}
func tableView(shouldCollapse cell: TableViewCell) {
cell.collapse()
}
Related
I am using TableView and on swiping the cell with editing style .delete but the size of cell has been customized as per the requirements of app. the delete Button height is little bigger how can we customize that.
Check the screenshot please:
#averydev update my code in swift you can try this
class CustomTableViewCell: UITableViewCell {
override func didTransition(to state: UITableViewCellStateMask) {
super.willTransition(to: state)
if state == .showingDeleteConfirmationMask {
let deleteButton: UIView? = subviews.first(where: { (aView) -> Bool in
return String(describing: aView).contains("Delete")
})
if deleteButton != nil {
deleteButton?.frame.size.height = 50.0
}
}
}
}
Currently, in iOS 16, the way to do it is by calling the willBeginEditingRowAt table view delegate method in your view controller class and changing the height property of the UISwipeActionPullView. I'm going to refer to your view controller class as NotesViewController here:
class NotesViewController: UIViewController {
...
extension NotesViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
// Get the subviews of the table view
let subviews = tableView.subviews
// Iterate through subviews to find _UITableViewCellSwipeContainerView
for subview in subviews {
if String(describing: type(of: subview)) == "_UITableViewCellSwipeContainerView" {
// Get the UISwipeActionPullView
if let swipeActionPullView = subview.subviews.first {
if String(describing: type(of: swipeActionPullView)) == "UISwipeActionPullView" {
// Set the height of the swipe action pull view (set to whatever height your custom cell is)
swipeActionPullView.frame.size.height = 50
}
}
}
}
}
}
}
I'm having an issue with my application architecture...
I have a UITableView filled with custom UITableViewCells.
Of course, I use dequeuing so there are only 8-10 cell instances ever generated.
Moving forward to my problem... I have added a theming feature to my application.
When the user long touches the main UINavigationBara notification is posted app wide that informs each viewController to update their UI.
When the viewController hosting the tableView with the custom cells receives the notification, it calls tableView.reloadData()
This works well, as I have implemented func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
Inside willDisplay I call a method on my custom tableViewCell which animates, using UIViewAnimation, appropriate colour changes to the cell.
It looks like this:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let newsFeedItemCell = cell as? NewsFeedItemCell {
if (self.nightModeEnabled) {
newsFeedItemCell.transitionToNightMode()
} else {
newsFeedItemCell.transitionFromNightMode()
}
}
}
Inside the custom cell implementation those methods look like this:
func transitionToNightMode() {
UIView.animate(withDuration: 1, animations: {
self.backgroundColor = UIColor.black
self.titleTextView.backgroundColor = UIColor.black
self.titleTextView.textColor = UIColor.white
})
}
func transitionFromNightMode() {
UIView.animate(withDuration: 1, animations: {
self.backgroundColor = UIColor.white
self.titleTextView.backgroundColor = UIColor.white
self.titleTextView.textColor = UIColor.black
})
}
This works fine... heres the issue:
Upon scrolling the tableView, any cells that weren't on screen have their colour update/animation code called as they scroll onto the screen, which leads to a jarring user experience.
I understand why this is happening of course, as willDisplay is only called as cells display.
I can't think of an elegant way to avoid this.
I'm happy that cells on screen are animating for the user experience to be pleasant, however, for cells off screen, I'd rather they skipped the animation.
Possible solutions (though inelegant):
Keep a reference to each of the 8-10 cells created by cellForRow, and check if they are off screen, if they are set their state immediately.
However, I don't like the idea of keeping a reference to each cell.
Any ideas?
I would not use a reloadData to animate this, since your data model for the tableview is not actually changing.
Instead, I would give the UITableView a function, like so:
class myClass : UITableViewController {
....
func transitionToNightMode() {
for visible in visibleCells {
UIView.animate(withDuration: 1, animations: {
visible.backgroundColor = UIColor.black
visible.titleTextView.backgroundColor = UIColor.black
visible.titleTextView.textColor = UIColor.white
})
}
}
}
and then in your willDisplay or cellForItemAt, set the correct appearance without animation.
I would try the following.
Instead of using UIView.animate, in transition(To/From)NightMode I would create a UIViewPropertyAnimator object that would do the same animation. I would keep the reference to that object around and then in prepare for reuse, if the animation is still running, I would simply finish that animation and reset the state. So something like this:
fileprivate var transitionAnimator: UIViewPropertyAnimator?
fileprivate func finishAnimatorAnimation() {
// if there is a transitionAnimator, immediately finishes it
if let animator = transitionAnimator {
animator.stopAnimation(false)
animator.finishAnimation(at: .end)
transitionAnimator = nil
}
}
override func prepareForReuse() {
super.prepareForReuse()
finishAnimatorAnimation()
}
func transitionToNightMode() {
transitionAnimator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut, animations: {
self.backgroundColor = UIColor.black
self.titleTextView.backgroundColor = UIColor.black
self.titleTextView.textColor = UIColor.white
})
transitionAnimator?.startAnimation()
}
func transitionFromNightMode() {
transitionAnimator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut, animations: {
self.backgroundColor = UIColor.white
self.titleTextView.backgroundColor = UIColor.white
self.titleTextView.textColor = UIColor.black
})
transitionAnimator?.startAnimation()
}
I have table view. Inside cell I have method that animates view inside cell.
func animateCell() {
UIView.animate(withDuration: 0.4, animations: { [weak self] in
self?.quantityBackround.transform = CGAffineTransform(scaleX: 25, y: 25)
}) { [weak self] _ in
UIView.animate(withDuration: 0.1) {
self?.quantityBackround.transform = CGAffineTransform.identity
}
}
}
Also I have prepareForReuse()
override func prepareForReuse() {
quantityBackround.transform = CGAffineTransform.identity
}
The animation must work only for last cell when array of datasource changes and I do this in property observer like this (fires when something is being added to array)
guard let cell = checkTableView.cellForRow(at: IndexPath(row: viewModel.checkManager.check.count - 1, section: 0)) as? CheckItemTableViewCell else { return }
cell.animateCell()
All of this works fine.
One problem, is that I encounter is that when tableView is reloaded, all background views in all cells expand from zero size to its initial. Last cell animates ok.
I think that i miss something in prepareForReuse and because of this i see this glitch of inreasing from zero to initial size.
How to fix it ?
You need to implement this method of UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
//here check if your this cell is the last one, something like this
if (indexPath.row == yourDataSourceArray.count - 1)
{
if let customCell = cell as? CheckItemTableViewCell{
customCell.animateCell()
}
}
}
Hope this helps
I am trying to make a expandable table view (static cells). A container view is placed inside a cell below the "Lists of options" cell, as shown below:
The problem comes with the animation when expanding / collapsing. The topmost cell of each section will disappear briefly(hidden?), then reappear after the animation ends. I have later experimented and the results are as followed:
This will not happen when using tableView.reloadData().
This will happen when using tableView.beginUpdates() / endUpdates() pairs.
Hence I have come to the conclusion that this has sth to do with the animation. Below is my code and pictures for further explanation for said issue.
Code for hiding / showing
private func hideContainerView(targetView: UIView) {
if targetView == violationOptionsContainerView {
violationOptionContainerViewVisible = false
}
tableView.beginUpdates()
tableView.endUpdates()
UIView.animate(withDuration: 0.1, animations: { targetView.alpha = 0.0 }) { _ in
targetView.isHidden = true
}
}
private func showContainerView(targetView: UIView) {
if targetView == violationOptionsContainerView {
violationOptionContainerViewVisible = true
}
tableView.beginUpdates()
tableView.endUpdates()
targetView.alpha = 0.0
UIView.animate(withDuration: 0.1, animations: { targetView.alpha = 1.0 }) { _ in
targetView.isHidden = false
}
}
Table view
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
switch cell {
case violationTableViewCell:
if violationOptionContainerViewVisible {
hideContainerView(targetView: violationOptionsContainerView)
} else {
showContainerView(targetView: violationOptionsContainerView)
}
default: break
}
// tableView.reloadData()
// if we use this instead of begin/end updates, the cells won't disappear. But then we'll be ditching animation too.
}
tableView.deselectRow(at: indexPath, animated: true)
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 && indexPath.row == 3 {
if !violationOptionContainerViewVisible {
return 0.0
}
}
return super.tableView(tableView, heightForRowAt: indexPath)
}
Images
Note that the first cell in each section ("FFFF" and the "slider bar") disappears and reappears during the animation
After searching for a few days, I have found the cause to this behavior.
I have, for some reason, set the cells layer.zposition below default zero. This will cause the cell to be seen "below" it's background view (hence disappeared) during the animation even though it's a subview of it.
Adjusting the value back to 0 or higher will remove this issue.
I have a tableView with cells populated by UIStackViews, with buttons as arrangedSubviews, that are created with a few functions.
The top view in the stackView is visible and all the other views are hidden, the button in the top views has an action, and when its called the other views should toggle between visible and hidden.
func generateButtons() -> [[UIButton]]{
var topButtonArray = [UIButton]()
var finalButtonArray = [[UIButton]]()
for title in array1 {
topButtonArray.append(createButton(title: title , action: "buttonPressed"))
}
for button in topButtonArray {
var buttonArray = [UIButton]()
buttonArray.append(button)
for title in array2 {
buttonArray.append(createButton(title: title, action: "moveOn"))
}
finalButtonArray.append(buttonArray)
}
return finalButtonArray
}
func generateStackViews() -> [UIStackView] {
stackViewArray = [UIStackView]()
let finalButtonArray = generateButtons()
for buttons in finalButtonArray{
stackViewArray.append(createStackView(subViews: buttons))
}
for stackView in stackViewArray{
let views = stackView.arrangedSubviews
let hiddenViews = views[1..<views.count]
for views in hiddenViews{
views.isHidden = true
}
}
return stackViewArray
}
func buttonPressed(){
//let stackViewArray = generateStackViews()
for stackView in stackViewArray{
let views = stackView.arrangedSubviews
let hiddenViews = views[1..<views.count]
for view in hiddenViews {
if view.isHidden == true{showViews(view: view)} else{hideViews(view: view)}
}
}
}
func showViews(view : UIView){
UIView.animate(withDuration: 0.3) {
view.isHidden = false
}
}
func hideViews(view : UIView) {
UIView.animate(withDuration: 0.2) {
view.isHidden = true
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "First")!
let stackViewArray = generateStackViews()
cell.contentView.addSubview(stackViewArray[indexPath.row])
return cell
}
Right now whats happening is that only the hidden views in the last cell are toggling between visible and hidden(no matter which cell i click) - I guess I need to instantiate the toggling on all cells but i cant figure out a way to do that.
another problem is that i want a top view to open only the hidden views in its cell, i figure i need to use indexPath.row somehow outside of 'cellForRowAt indexPath'.
You'll thank your sanity if you move a lot of this logic into a UITableViewCell subclass.
Not a complete rewrite of your snippet (hinting at setting up some of the views via storyboard, but no big difference to doing in code except without storyboard you'll also need to override the cell's init and set up the subviews), but here's a starting point that you could investigate:
class StackViewCell: UITableViewCell {
// these could be set up in code in the `init` method if
// you don't want to use storyboards
#IBOutlet var stackView: UIStackView!
#IBOutlet var toggleButton: UIButton!
var optionButtons: [UIButton] = [] {
didSet {
for button in optionButtons {
button.isHidden = optionsAreHidden
stackView.addArrangedSubview(button)
}
}
}
// iterates over buttons to change hidden property based on `optionsAreHidden` property
var optionsAreHidden: Bool = true {
didSet {
optionButtons.forEach({ $0.isHidden = optionsAreHidden })
}
}
#IBAction func toggleButtonPressed(button: UIButton) {
optionsAreHidden = !optionsAreHidden
}
// set up stackview and toggle button here if not setting up in storyboard
//init?(coder aDecoder: NSCoder) { }
}
Then the view controller becomes a lot simpler. It's not clear to me if each cell's stack view has the same set of option buttons, or if the option buttons are somehow contextual based on which row they're in.
If they're all the same I'd also move the generateOptionsButtons() logic into the StackViewCell (or actually if they're the same for each cell I'd probably set them up in the storyboard).
class OptionsViewController: UITableViewController {
func generateOptionsButtons() -> [UIButton] {
// create and return buttons for a cell
// potentially move this into `StackViewCell` too...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StackViewCellIdentifier", for: indexPath)
if let stackViewCell = cell as? StackViewCell {
stackViewCell.optionButtons = generateOptionsButtons()
}
return cell
}
}