UIView animation in UITableView doesn't work after reload data - ios

I am facing a strange bug in my app right now. I have an extension for UIView that rotates an indicator 90 degrees to the left and right. I am using an UITableViewController as a navigation drawer. I also added an expandable list and use an UITableViewHeaderFooterView for the main areas (which have subitems).
The UITableViewHeaderFooterView are like the main 'cells'. And below them are real cells of the UITableView that are deleted and inserted when the user taps the UITableViewHeaderFooterView.
Depending on the URL the WebView (main view) has, I want to update the content of the tableView. As soon as the data changes the animation of the extension no longer works. The code still gets executed and the values are correct. I just don't see the animation anymore.
The UITableViewHeaderFooterView:
import UIKit
class TableSectionHeader: UITableViewHeaderFooterView {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var indicatorImage: UIImageView!
override func draw(_ rect: CGRect) {
super.draw(rect)
indicatorImage.image = IconFont.image(fromIcon: .next, size: Constants.Sizes.MENU_INDICATOR_SIZE, color: Constants.Colors.WHITE_COLOR)
}
func setExpanded(expanded: Bool) {
indicatorImage.rotate(expanded ? .pi / 2 : 0.0)
}
}
The method inside the WebViewController:
private func loadNewMenu(href: String) {
NetworkService.sharedInstance.getNavigation(path: href, completion: { menu in
if let menuController = self.menuVC {
menuController.menuItems = menu
menuController.tableView.reloadData()
}
})
}
The UIView extension:
import UIKit
extension UIView {
func rotate(_ toValue: CGFloat, duration: CFTimeInterval = 0.2) {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.toValue = toValue
animation.duration = duration
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
self.layer.add(animation, forKey: "rotationIndicator")
}
}
The relevant methods for the MenuTableViewController:
// MARK: Custom Menu Methods
#objc func handleExpandClose(sender: UITapGestureRecognizer) {
guard let section = sender.view?.tag else {
return
}
var indexPaths = [IndexPath]()
for row in subItems.indices {
let indexPath = IndexPath(row: row, section: section)
indexPaths.append(indexPath)
}
let isExpanded = menuItems[section].isExpanded
menuItems[section].isExpanded = !isExpanded
sectionHeaders[section].setExpanded(expanded: !isExpanded)
if isExpanded {
tableView.deleteRows(at: indexPaths, with: .fade)
} else {
tableView.beginUpdates()
closeOpenedSections(section: section)
tableView.insertRows(at: indexPaths, with: .fade)
tableView.endUpdates()
}
}
private func closeOpenedSections(section: Int) {
var closeIndexPaths = [IndexPath]()
for (sectionIndex, sec) in menuItems.enumerated() {
guard let subItems = sec.subItems, sec.isExpanded, sectionIndex != section else {
continue
}
sec.isExpanded = false
for rowIndex in subItems.indices {
let closeIndexPath = IndexPath(row: rowIndex, section: sectionIndex)
closeIndexPaths.append(closeIndexPath)
}
sectionHeaders[sectionIndex].setExpanded(expanded: false)
tableView.deleteRows(at: closeIndexPaths, with: .fade)
}
}
That's the implementation of the viewForHeaderInSection in my MenuTableViewController:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let sectionHeader = self.tableView.dequeueReusableHeaderFooterView(withIdentifier: "TableSectionHeader") as? TableSectionHeader {
sectionHeader.setUpHeader(emphasize: menuItems[section].emphasize)
sectionHeader.titleLabel.text = menuItems[section].title
let tap = UITapGestureRecognizer(target: self, action: #selector(MenuTableViewController.handleExpandClose(sender:)))
sectionHeader.addGestureRecognizer(tap)
sectionHeader.indicatorImage.isHidden = menuItems[section].subItems != nil ? false : true
sectionHeader.tag = section
self.sectionHeaders.append(sectionHeader)
return sectionHeader
}
return UIView()
}
I know it's a lot of code, but I guess it's somehow related to the reloadData() of the tableView. The animation works perfectly fine before I perform the reload. If I have not opened the menu before it gets reloaded I also don't see the bug. It just happens after the first reload of the data (even if the data is exactly the same).
Again, the animation code still gets executed and the expanded value is the correct one (true/false). I've printed the UIImageView on which the animation takes place to the console and it seems to be the identical view even after the reload. But the animation doesn't appear.

It turned out to be a mistake by myself! I was using two arrays (sectionHeaders, menuItems). I only changed one of them when I received new data. Therefore, there was a mismatch between the cells and the content I was working on.

Related

How to show/hide label in a different view when UISwitch isOn in swift?

I have a UISwitch component in my CreateSomethingViewController. This component is on a xib file.
In my SomethingTableViewCell, I have a label called existsLabel.
When I create my something, I can select as Existent (if I turn my UISwitch component on) or not (if Switch is off).
If my existsLabel was in my CreateSomethingViewController, I would do something like this:
#IBAction func changeSomethingExistence(_ sender: UISwitch) {
let isExistent = sender.isOn
existsLabel.isHidden = false
if isExistent {
existsLabel.isHidden = true
}
}
How can I do this (show my existsLabel on my SomethingTableViewCell) when my UISwitch isOn? Using swift.
I think, you already knew the index or position of your updated objects. So We can reload only visible cells row after updating on particular objects to the index position of your cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? YourTableViewCell
cell?.yourSwitch.isOn = yourList[indexPath.row].switchIsOne
cell?.yourSwitch.tag = indexPath.row
cell?.yourSwitch.addTarget(self, action: #selector(changeSomethingExistence), for:UIControl.Event.valueChanged)
cell?.existsLabel.isHidden = !yourList[indexPath.row].switchIsOne
return cell!
}
Here is your Switch update actions:
#objc func changeSomethingExistence(mySwitch: UISwitch) {
yourList[mySwitch.tag].switchIsOne = mySwitch.isOn
self.updateCell(indexRow: mySwitch.tag)
}
Call this function from anywhere with your selected index and update the same.
func updateCell(indexRow: Int) {
let updatedIndexPath = IndexPath(row: indexRow, section: 0)
self.tableView.reloadRows(at: [updatedIndexPath], with: .automatic)
}
Here's an example. Instead of hiding and showing a view, I set the background color of the cells. The basic ideas are the same.
Essentially you need an object to store the value that the switch controls. In this case I store that data in the same object that I used as the UITableViewDataSource. When the switch is flipped, you tell that object to change the value. It will broadcast the change to all the cells that are currently listening for the change.
There are lots of ways you could observe the change. You could use the Target Action pattern, you could broadcast the change using the NSNotificationCenter. You could use key/value observers, etc. In this case the object holding the value has an #Published property and the cells subscribe to that property.
One critical thing to do is implement prepareForReuse. When a cell is scrolled off the view, it is put in a reuse queue. Rather than create a new cell the system might hand you one out of the reuse buffer. If it does that, you want to be sure the cell is listening to the right source of information for things that change dynamically.
You should be able to copy/paste this code into an iOS Playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
import Combine
class CustomCell : UITableViewCell {
var cancelBackgrounds : AnyCancellable?
override func prepareForReuse() {
cancelBackgrounds?.cancel()
cancelBackgrounds = nil
// ALWAYS call super... this can cause hard to identify bugs
super.prepareForReuse()
}
func observeFancyBackground(dataSource: TableData) {
// Set up to observe when the fanch Background value changes
// If this cell was listening to someone else, stop listening to them
// and start listeneing to the new guy.
// This may not be necessary - its a safety check.
cancelBackgrounds?.cancel()
cancelBackgrounds = nil
// Start listening to the new information source
cancelBackgrounds = dataSource.$showFancyBackgrounds.sink(receiveValue: {
isOn in
self.setBackground(isOn)
})
}
private func setBackground(_ showFancy: Bool) {
if showFancy {
self.backgroundConfiguration?.backgroundColor = UIColor.yellow
} else {
self.backgroundConfiguration?.backgroundColor = UIColor.white
}
}
}
class TableData : NSObject, UITableViewDataSource {
let tableData = (1...1000).map { "\(Int($0))" }
#Published var showFancyBackgrounds = false
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.textLabel?.text = tableData[indexPath.row]
cell.observeFancyBackground(dataSource: self)
return cell
}
}
class MyViewController : UIViewController {
let switchView = UISwitch()
let tableView = UITableView(frame: CGRect(x: 0, y: 200, width: 320, height: 100), style: .plain)
let tableData = TableData()
// This is the action called when the switch is toggled.
#objc func switchFlipped(sender: UISwitch) {
tableData.showFancyBackgrounds = sender.isOn
}
// This just sets things up to be pretty.
override func loadView() {
let view = UIView()
switchView.translatesAutoresizingMaskIntoConstraints = false
switchView.addTarget(self, action: #selector(switchFlipped), for: .valueChanged)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
tableView.dataSource = tableData
view.addSubview(switchView)
view.addSubview(tableView)
self.view = view
let viewIDs = ["switch" : switchView,
"table" : tableView]
let constraints = [
NSLayoutConstraint.constraints(
withVisualFormat: "V:|-8-[switch]-[table]-|",
options: [],
metrics: nil,
views: viewIDs),
NSLayoutConstraint.constraints(
withVisualFormat: "|-[switch]-|",
options: [],
metrics: nil,
views: viewIDs),
NSLayoutConstraint.constraints(
withVisualFormat: "|-0-[table]-0-|",
options: [],
metrics: nil,
views: viewIDs),
].flatMap { $0 }
view.addConstraints(constraints)
}
}
let myViewController = MyViewController()
PlaygroundPage.current.liveView = myViewController
You can do this by reloading the tableView when the switch is changed.
var isExistent: Bool
#IBAction func changeSomethingExistence(_ sender: UISwitch) {
isExistent = sender.isOn
//reload the table
tableView.reloadData()
}
In your UITableViewDataSource you can check which cell.label need to be hidden or not and accordingly hide/show the label of those cells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//decide which cells needs to be hide/show based on the indexPath and switchValue
//then you can call cell.existsLabel.isHidden = isExistent
}

UITableView loads only custom cells that are visible on device and crashes when one more cell is added

I use an empty UITableView with custom cells and I add new items one by one without any problem. The tableView is scrollable, however when I add an item to the cell that is one index more from the last visible cell the app crashes.
When the app is loaded the numberOfRowsinSection is 1 and with every new entry it grows by 1. If the device has 10 visible cells it crashes on 11. If the device has 6 visible cells it crashes on 7. The app unexpectedly finds nil while unwrapping an Optional value.
Using advices from the question titled UITableview Not scrolling?
I tried each of the following lines in viewDidLoad and in my function:
self.myTableView.delegate = self
self.myTableView.autoresizingMask = UIView.AutoresizingMask.flexibleHeight;
self.myTableView.rowHeight = UITableView.automaticDimension
self.myTableView.bounces = true;
self.myTableView.reloadData()
without any positive result.
Here is the code:
var enterCounts: Int = 1
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return enterCounts
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextInputCell") as! TextInputTableViewCell
return cell
}
#IBAction func enter(_ sender: Any) {
let activeRow = self.enterCounts - 1
let index = IndexPath(row: activeRow, section: 0)
let cell: TextInputTableViewCell = self.myTableView.cellForRow(at: index) as! TextInputTableViewCell
if cell.myTextField.text == "" {
"DO NOTHING"
} else {
"DO STUFF"
enterCounts += 1
self.myTableView.reloadData()
let nextIndex = IndexPath(row: activeRow + 1, section: 0)
"This is the line that finds nil and crashes when row is out of view"
let nextCell: TextInputTableViewCell = self.myTableView.cellForRow(at: nextIndex) as! TextInputTableViewCell
nextCell.myTextField.text = ""
nextCell.myTextField.becomeFirstResponder()
}
}
I would expect the UITableView to scroll and keep on loading as many cells the user enters, exactly as it does with the first/visible cells.Thank you.
After the 2 answers the code is:
#IBAction func enter(_ sender: Any) {
let activeRow = self.enterCounts - 1
let index = IndexPath(row: activeRow, section: 0)
let cell: TextInputTableViewCell = self.myTableView.cellForRow(at: index) as! TextInputTableViewCell
if cell.myTextField.text == "" {
"DO NOTHING"
} else {
"DO STUFF"
enterCounts += 1
let nextIndex = IndexPath(row: activeRow + 1, section: 0)
self.myTableView.insertRows(at: [nextIndex], with: .automatic)
self.myTableView.scrollToRow(at: nextIndex,at: .middle, animated: true)
//These lines are executed only when I am in visible cells
//when a new cell is added it is not ready to become first responder it is skipped and entered text is getting mixed up.
if let nextCell: TextInputTableViewCell = self.myTableView.cellForRow(at: nextIndex) as? TextInputTableViewCell {
nextCell.myTextField.text = ""
nextCell.myTextField.becomeFirstResponder()
}
}
}
With the code above the new cells appear wonderfully but textField become first responder only once, for the first cell that appears in view.
I declare my custom cell class in as below
#IBOutlet weak var myTextField: UITextField!
public func configure(text: String?, placeholder: String) {
myTextField.text = text
// myTextField.placeholder = placeholder
myTextField.accessibilityValue = text
// myTextField.accessibilityLabel = placeholder
}
override public func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override public func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
If I use a textField outside the tableView and keep the tableView only for displaying my entered values things are simple but having for entryField the last cell of the tableView creates problems when I try to make first responder the textField of the new inserted cell.
if you need to add new cell you can use this line to add it:
let indexPath = IndexPath(row: ... , section: ...)
tableView.insertRows(at: [indexPath], with: .automatic)
after that scroll to it
tableView.scrollToRow(at: indexPath,at: .middle, animated: true)
finally, you can use this cell
let cell = tableView.cellForRow(at: nextIndex) as! YourCustomCellClass
It is crashing because apple only keeps cells in memory that are visible, In your case you are access cell that is not in memory and instead to use optional you are forcing to unwrap which causes the crash.
After knowing this you should handle exception for cells that are not visible, like bewlow
#IBAction func enter(_ sender: Any) {
let activeRow = self.enterCounts - 1
let index = IndexPath(row: activeRow, section: 0)
let cell: TextInputTableViewCell = self.myTableView.cellForRow(at: index) as! TextInputTableViewCell
if cell.myTextField.text == "" {
"DO NOTHING"
} else {
"DO STUFF"
enterCounts += 1
self.myTableView.reloadData()
let nextIndex = IndexPath(row: activeRow + 1, section: 0)
//"This is the line that finds nil and crashes when row is out of view"
if let nextCell: TextInputTableViewCell = self.myTableView.cellForRow(at: nextIndex) as? TextInputTableViewCell
{
nextCell.myTextField.text = ""
nextCell.myTextField.becomeFirstResponder()
}
}
Or
if you want to make your textfield first responder first get cell in memory by scrolling to index Or by inserting it and then access it.
if you want to add new line in you tableview whenever a enter button is tapped this I think you should try doing it this way
I am assuming that u just want a textfield in your tableview but this could work with other thing also
in you custom class make an outlet for your textfield name it whatever you want I am naimg it tf and do this
iboutlet weak var tf: uitextfield!{
didSet{
tf.delegate = self
}}
and create a closure var like this
var textsaved: ((String) -> Void)?
then add textfield delegate to your customcell class like this
extension CustomCell: uitextfielddelegate{ }
then in your extension write :
func textfieldshouldreturn(_ textfield: uitextfield) -> Bool{
tf.resignFirstResponder()
retrun true }
func textfielddidendediting(_ textfield: uitextfield){
textsaved?(textfield.text!) }
then in your view controller create an empty array of string
var myarr = [String]()
make outlet for enter button and tableview
#iboutlet weak var mytableview: uitableView!{
didSet{
mytableview.delegate = self
mytableview.datasource = self }
#iboutlet weak var enterBtn(_ sender: uibutton) {
myarr.append("")
mytableview.reloaddata() }
in number of rows
return myarr.count
in cell for row at
let cell = tableview.dequereuseablecell(withidentifier: "cell", for :indexpath) as! customcell
cell.tf.text = myarr[indexpath.row]
cell.textsaved = { [unowned self] (text) in
self.myarr.remove(at: indexpath.row)
self.myarr.insert(text, at: indexpath.row)
sel.mytableview.reloaddata()
} return cell }

Properly delegate button action from custom Cell to delete rows in UITableView

Still very much a Swift noob, I have been looking around for a proper way/best practice to manage row deletions in my UITableView (which uses custom UserCells) based on tapping a UIButton inside the UserCell using delegation which seems to be the cleanest way to do it.
I followed this example: UITableViewCell Buttons with action
What I have
UserCell class
protocol UserCellDelegate {
func didPressButton(_ tag: Int)
}
class UserCell: UITableViewCell {
var delegate: UserCellDelegate?
let addButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Add +", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
addSubview(addButton)
addButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -6).isActive = true
addButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
addButton.heightAnchor.constraint(equalToConstant: self.frame.height / 2).isActive = true
addButton.widthAnchor.constraint(equalToConstant: self.frame.width / 6).isActive = true
}
func buttonPressed(_ sender: UIButton) {
delegate?.didPressButton(sender.tag)
}
}
TableViewController class:
class AddFriendsScreenController: UITableViewController, UserCellDelegate {
let cellId = "cellId"
var users = [User]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! UserCell
cell.delegate = self
cell.tag = indexPath.row
return cell
}
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
where the Users in users are appended with a call to the database in the view controller.
My issues
The button in each row of the Table View is clickable but does not do anything
The button seems to be clickable only when doing a "long press", i.e. finger stays on it for a ~0.5s time
Will this method guarantee that the indexPath is updated and will not fall out of scope ? I.e. if a row is deleted at index 0, will deleting the "new" row at index 0 work correctly or will this delete the row at index 1 ?
What I want
Being able to click the button in each row of the table, which would remove it from the tableview.
I must be getting something rather basic wrong and would really appreciate if a Swift knight could enlighten me.
Many thanks in advance.
There are at least 3 issues in your code:
In UserCell you should call:
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
once your cell has been instantiated (say, from your implementation of init(style:reuseIdentifier:)) so that self refers to an actual instance of UserCell.
In AddFriendsScreenController's tableView(_:cellForRowAt:) you are setting the tag of the cell itself (cell.tag = indexPath.row) but in your UserCell's buttonPressed(_:) you are using the tag of the button. You should modify that function to be:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
delegate?.didPressButton(self.tag)
}
As you guessed and as per Prema Janoti's answer you ought to reload you table view once you deleted a row as your cells' tags will be out of sync with their referring indexPaths. Ideally you should avoid relying on index paths to identify cells but that's another subject.
EDIT:
A simple solution to avoid tags being out of sync with index paths is to associate each cell with the User object they are supposed to represent:
First add a user property to your UserCell class:
class UserCell: UITableViewCell {
var user = User() // default with a dummy user
/* (...) */
}
Set this property to the correct User object from within tableView(_:cellForRowAt:):
//cell.tag = indexPath.row
cell.user = self.users[indexPath.row]
Modify the signature of your UserCellDelegate protocol method to pass the user property stored against the cell instead of its tag:
protocol UserCellDelegate {
//func didPressButton(_ tag: Int)
func didPressButtonFor(_ user: User)
}
Amend UserCell's buttonPressed(_:) action accordingly:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
//delegate?.didPressButton(self.tag)
delegate?.didPressButtonFor(self.user)
}
Finally, in your AddFriendsScreenController, identify the right row to delete based on the User position in the data source:
//func didPressButton(_ tag: Int) { /* (...) */ } // Scrap this.
func didPressButtonFor(_ user: User) {
if let index = users.index(where: { $0 === user }) {
let indexPath = IndexPath(row: index, section: 0)
users.remove(at: index)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
Note the if let index = ... construct (optional binding) and the triple === (identity operator).
This downside of this approach is that it will create tight coupling between your User and UserCell classes. Best practice would dictate using a more complex MVVM pattern for example, but that really is another subject...
There is a lot of bad/old code on the web, even on SO. What you posted has "bad practice" written all over it. So first a few pointers:
Avoid an UITableViewController at all cost. Have a normal view controller with a table view on it
Delegates should always be weak unless you are 100% sure what you are doing
Be more specific when naming protocols and protocol methods
Keep everything private if possible, if not then use fileprivate. Only use the rest if you are 100% sure it is a value you want to expose.
Avoid using tags at all cost
The following is an example of responsible table view with a single cell type which has a button that removes the current cell when pressed. The whole code can be pasted into your initial ViewController file when creating a new project. In storyboard a table view is added constraint left, right, top, bottom and an outlet to the view controller. Also a cell is added in the table view with a button in it that has an outlet to the cell MyTableViewCell and its identifier is set to "MyTableViewCell".
The rest should be explained in the comments.
class ViewController: UIViewController {
#IBOutlet private weak var tableView: UITableView? // By default use private and optional. Always. For all outlets. Only expose it if you really need it outside
fileprivate var myItems: [String]? // Use any objects you need.
override func viewDidLoad() {
super.viewDidLoad()
// Attach table viw to self
tableView?.delegate = self
tableView?.dataSource = self
// First refresh and reload the data
refreshFromData() // This is to ensure no defaults are visible in the beginning
reloadData()
}
private func reloadData() {
myItems = nil
// Simulate a data fetch
let queue = DispatchQueue(label: "test") // Just for the async example
queue.async {
let items: [String] = (1...100).flatMap { "Item: \($0)" } // Just generate some string
Thread.sleep(forTimeInterval: 3.0) // Wait 3 seconds
DispatchQueue.main.async { // Go back to main thread
self.myItems = items // Assign data source to self
self.refreshFromData() // Now refresh the table view
}
}
}
private func refreshFromData() {
tableView?.reloadData()
tableView?.isHidden = myItems == nil
// Add other stuff that need updating here if needed
}
/// Will remove an item from the data source and update the array
///
/// - Parameter item: The item to remove
fileprivate func removeItem(item: String) {
if let index = myItems?.index(of: item) { // Get the index of the object
tableView?.beginUpdates() // Begin updates so the table view saves the current state
myItems = myItems?.filter { $0 != item } // Update our data source first
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade) // Do the table view cell modifications
tableView?.endUpdates() // Commit the modifications
}
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myItems?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell {
cell.item = myItems?[indexPath.row]
cell.delegate = self
return cell
} else {
return UITableViewCell()
}
}
}
// MARK: - MyTableViewCellDelegate
extension ViewController: MyTableViewCellDelegate {
func myTableViewCell(pressedMainButton sender: MyTableViewCell) {
guard let item = sender.item else {
return
}
// Delete the item if main button is pressed
removeItem(item: item)
}
}
protocol MyTableViewCellDelegate: class { // We need ": class" so the delegate can be marked as weak
/// Called on main button pressed
///
/// - Parameter sender: The sender cell
func myTableViewCell(pressedMainButton sender: MyTableViewCell)
}
class MyTableViewCell: UITableViewCell {
#IBOutlet private weak var button: UIButton?
weak var delegate: MyTableViewCellDelegate? // Must be weak or we can have a retain cycle and create a memory leak
var item: String? {
didSet {
button?.setTitle(item, for: .normal)
}
}
#IBAction private func buttonPressed(_ sender: Any) {
delegate?.myTableViewCell(pressedMainButton: self)
}
}
In your case the String should be replaced by the User. Next to that you will have a few changes such as the didSet in the cell (button?.setTitle(item.name, for: .normal) for instance) and the filter method should use === or compare some id or something.
try this -
update didPressButton method like below -
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.reloadData()
}

Action affecting buttons in all TableView Headers

I am using a custom UITableViewHeaderFooterView for me TableView. I was trying to implement hiding and showing rows in a section(which I have working). I decided to add a button (>) to the section header so that I can rotate it when the section is "expanded/collapsed".
The problem I have appears when I click the button. When the rotateCollapseButton() function is called, the (>) buttons in all the section headers rotate, not just the one that was clicked. Sometimes it'll even exclude the button that was clicked or clicking one will affect a different one and not itself.
How can I make it so that only the correct button will rotate?
This is the code I have for the custom Header I created.
var rotated:Bool = false
var section:Int?
weak var delegate:MessageGroupHeaderDelegate?
#IBAction func expandCollapseButtonClicked(_ sender: Any) {
rotateCollapseButton(sender as! UIButton)
delegate?.didPressExpandCollapseButton(atSection : self.section!)
}
func rotateCollapseButton(_ button:UIButton) {
UIView.animate(withDuration: 0.5) { () -> Void in
var rotationAngle:CGFloat = CGFloat(M_PI_2)
if self.rotated {
rotationAngle = CGFloat(0)
}
button.transform = CGAffineTransform(rotationAngle : rotationAngle)
self.rotated = !self.rotated
}
}
EDIT: Code where the header is initialized...
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// Dequeue with the reuse identifier
let cell = self.massMessageGroupsTableView.dequeueReusableHeaderFooterView(withIdentifier: "MessageGroupTableViewHeader")
let header = cell as! MessageGroupTableViewHeader
header.groupNameLabel.text = messageGroupsMap[section]?.messageGroup.name
header.section = section
header.setComposeButtonImage()
header.delegate = self
return cell
}
Thank you!
In your header setting, trying doing this instead:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// Dequeue with the reuse identifier
let cell = self.massMessageGroupsTableView.dequeueReusableCell(withIdentifier: "MessageGroupTableViewHeader")
let header = cell as! MessageGroupTableViewHeader
header.groupNameLabel.text = messageGroupsMap[section]?.messageGroup.name
header.section = section
header.setComposeButtonImage()
header.delegate = self
let containingView : UIView = UIView()
containingView.addSubview(header)
return containingView
}

Using long press gesture to reorder cells in tableview?

I want to be able to reorder tableview cells using a longPress gesture (not with the standard reorder controls). After the longPress is recognized I want the tableView to essentially enter 'edit mode' and then reorder as if I was using the reorder controls supplied by Apple.
Is there a way to do this without needing to rely on 3rd party solutions?
Thanks in advance.
EDIT: I ended up using the solution that was in the accepted answer and relied on a 3rd party solution.
They added a way in iOS 11.
First, enable drag interaction and set the drag and drop delegates.
Then implement moveRowAt as if you are moving the cell normally with the reorder control.
Then implement the drag / drop delegates as shown below.
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
tableView.dropDelegate = self
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { }
extension TableView: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
return [UIDragItem(itemProvider: NSItemProvider())]
}
}
extension TableView: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if session.localDragSession != nil { // Drag originated from the same app.
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UITableViewDropProposal(operation: .cancel, intent: .unspecified)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
}
}
Swift 3 and no third party solutions
First, add these two variables to your class:
var dragInitialIndexPath: IndexPath?
var dragCellSnapshot: UIView?
Then add UILongPressGestureRecognizer to your tableView:
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(onLongPressGesture(sender:)))
longPress.minimumPressDuration = 0.2 // optional
tableView.addGestureRecognizer(longPress)
Handle UILongPressGestureRecognizer:
// MARK: cell reorder / long press
func onLongPressGesture(sender: UILongPressGestureRecognizer) {
let locationInView = sender.location(in: tableView)
let indexPath = tableView.indexPathForRow(at: locationInView)
if sender.state == .began {
if indexPath != nil {
dragInitialIndexPath = indexPath
let cell = tableView.cellForRow(at: indexPath!)
dragCellSnapshot = snapshotOfCell(inputView: cell!)
var center = cell?.center
dragCellSnapshot?.center = center!
dragCellSnapshot?.alpha = 0.0
tableView.addSubview(dragCellSnapshot!)
UIView.animate(withDuration: 0.25, animations: { () -> Void in
center?.y = locationInView.y
self.dragCellSnapshot?.center = center!
self.dragCellSnapshot?.transform = (self.dragCellSnapshot?.transform.scaledBy(x: 1.05, y: 1.05))!
self.dragCellSnapshot?.alpha = 0.99
cell?.alpha = 0.0
}, completion: { (finished) -> Void in
if finished {
cell?.isHidden = true
}
})
}
} else if sender.state == .changed && dragInitialIndexPath != nil {
var center = dragCellSnapshot?.center
center?.y = locationInView.y
dragCellSnapshot?.center = center!
// to lock dragging to same section add: "&& indexPath?.section == dragInitialIndexPath?.section" to the if below
if indexPath != nil && indexPath != dragInitialIndexPath {
// update your data model
let dataToMove = data[dragInitialIndexPath!.row]
data.remove(at: dragInitialIndexPath!.row)
data.insert(dataToMove, at: indexPath!.row)
tableView.moveRow(at: dragInitialIndexPath!, to: indexPath!)
dragInitialIndexPath = indexPath
}
} else if sender.state == .ended && dragInitialIndexPath != nil {
let cell = tableView.cellForRow(at: dragInitialIndexPath!)
cell?.isHidden = false
cell?.alpha = 0.0
UIView.animate(withDuration: 0.25, animations: { () -> Void in
self.dragCellSnapshot?.center = (cell?.center)!
self.dragCellSnapshot?.transform = CGAffineTransform.identity
self.dragCellSnapshot?.alpha = 0.0
cell?.alpha = 1.0
}, completion: { (finished) -> Void in
if finished {
self.dragInitialIndexPath = nil
self.dragCellSnapshot?.removeFromSuperview()
self.dragCellSnapshot = nil
}
})
}
}
func snapshotOfCell(inputView: UIView) -> UIView {
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)
inputView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let cellSnapshot = UIImageView(image: image)
cellSnapshot.layer.masksToBounds = false
cellSnapshot.layer.cornerRadius = 0.0
cellSnapshot.layer.shadowOffset = CGSize(width: -5.0, height: 0.0)
cellSnapshot.layer.shadowRadius = 5.0
cellSnapshot.layer.shadowOpacity = 0.4
return cellSnapshot
}
You can't do it with the iOS SDK tools unless you want to throw together your own UITableView + Controller from scratch which requires a decent amount of work. You mentioned not relying on 3rd party solutions but my custom UITableView class can handle this nicely. Feel free to check it out:
https://github.com/bvogelzang/BVReorderTableView
So essentially you want the "Clear"-like row reordering right? (around 0:15)
This SO post might help.
Unfortunately I don't think you can do it with the present iOS SDK tools short of hacking together a UITableView + Controller from scratch (you'd need to create each row itself and have a UITouch respond relevant to the CGRect of your row-to-move).
It'd be pretty complicated since you need to get the animation of the rows "getting out of the way" as you move the row-to-be-reordered around.
The cocoas tool looks promising though, at least go take a look at the source.
There's a great Swift library out there now called SwiftReorder that is MIT licensed, so you can use it as a first party solution. The basis of this library is that it uses a UITableView extension to inject a controller object into any table view that conforms to the TableViewReorderDelegate:
extension UITableView {
private struct AssociatedKeys {
static var reorderController: UInt8 = 0
}
/// An object that manages drag-and-drop reordering of table view cells.
public var reorder: ReorderController {
if let controller = objc_getAssociatedObject(self, &AssociatedKeys.reorderController) as? ReorderController {
return controller
} else {
let controller = ReorderController(tableView: self)
objc_setAssociatedObject(self, &AssociatedKeys.reorderController, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return controller
}
}
}
And then the delegate looks somewhat like this:
public protocol TableViewReorderDelegate: class {
// A series of delegate methods like this are defined:
func tableView(_ tableView: UITableView, reorderRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
}
And the controller looks like this:
public class ReorderController: NSObject {
/// The delegate of the reorder controller.
public weak var delegate: TableViewReorderDelegate?
// ... Other code here, can be found in the open source project
}
The key to the implementation is that there is a "spacer cell" that is inserted into the table view as the snapshot cell is presented at the touch point, so you need to handle the spacer cell in your cellForRow:atIndexPath: call:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let spacer = tableView.reorder.spacerCell(for: indexPath) {
return spacer
}
// otherwise build and return your regular cells
}
Sure there's a way. Call the method, setEditing:animated:, in your gesture recognizer code, that will put the table view into edit mode. Look up "Managing the Reordering of Rows" in the apple docs to get more information on moving rows.

Resources