How to replace closure or delegation callback which get called when user tap on a button of a table view cell with Combine framework ?
problem - if Subscriber is added from the view controller and store returned AnyCancellable in a Set ; 1. storage of anyCancellable is getting heigh as cell return .2. many subscribers receive value when user tap on one button of a cell
I used built in subscriber Sink
In ViewController
var myAnyCancellableSet: Set<AnyCancellable> = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mycell")
cell.doSomethingSubject.sink {
print("user tap button on cell")
}.store(in: &myAnyCancellableSet)
return cell
}
In tableview cell
import UIKit
import Combine
class MyTableViewCell: UITableViewCell {
private lazy var myDoSomethingSubject = PassthroughSubject<Void, Never>()
lazy var doSomethingSubject = myDoSomethingSubject.eraseToAnyPublisher()
#IBAction func buttonTapped(_ sender: UIButton) {
myDoSomethingSubject.send()
}
}
Don't store all your tickets together. Store them by index path.
var ticketForIndexPath: [IndexPath: AnyCancellable] = [:]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mycell") as! MyCell
ticketForIndexPath[indexPath] = cell.doSomethingSubject.sink {
print("user tap button on cell")
}
return cell
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
ticketForIndexPath[indexPath] = nil
}
Related
I am new in programming and iOS Development, I need to make table view that has multiple limited checkmark.
I mean, I want to allow the user to select maximum 3 items (not just 1, but also not all of item in the table view can be selected) in the table view, I have tried but I haven't gotten what I want, I just can select one only item in table view
here is the code I use
import UIKit
class CreateEventStep2VC: UIViewController {
#IBOutlet weak var eventTypeNameLabel: UILabel!
#IBOutlet weak var tableView: UITableView!
var newEvent : [String:Any]!
var eventTypeAvailableData = [String]()
var selectedEventTypes = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// initial value
eventTypeNameLabel.text = ""
// get event Type Data list from EventType data model
eventTypeAvailableData = EventType.allValues.map { $0.toString() }
}
}
extension CreateEventStep2VC : UITableViewDataSource {
//MARK: - UITableViewDatasource
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventTypeAvailableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EventTypeCell", for: indexPath) as! CreateEventStep2Cell
cell.eventTypeNames = eventTypeAvailableData[indexPath.row]
return cell
}
}
extension CreateEventStep2VC : UITableViewDelegate {
//MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .checkmark
}
}
}
could you please help me ?
You can't simply add the checkmark to the cell; cell objects will be re-used as the tableview scrolls, so you will lose checkmarks and end up with checkmarks in cells that shouldn't have them.
You need to track the checked cells in another structure; I suggest using a Set<IndexPath>. You can either allow multi-selection in your tableview, or (my preference) deselect the row after you add the checkmark.
You also need to ensure that your cellForRowAt: sets the accessory type correctly
class CreateEventStep2VC: UIViewController {
#IBOutlet weak var eventTypeNameLabel: UILabel!
#IBOutlet weak var tableView: UITableView!
var newEvent : [String:Any]!
var eventTypeAvailableData = [String]()
var selectedEventTypes = Set<IndexPath>()
override func viewDidLoad() {
super.viewDidLoad()
// initial value
eventTypeNameLabel.text = ""
// get event Type Data list from EventType data model
eventTypeAvailableData = EventType.allValues.map { $0.toString() }
}
}
extension CreateEventStep2VC : UITableViewDataSource {
//MARK: - UITableViewDatasource
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventTypeAvailableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EventTypeCell", for: indexPath) as! CreateEventStep2Cell
cell.eventTypeNames = eventTypeAvailableData[indexPath.row]
cell.accessoryType = selectedEventTypes.contains(indexPath) ? .checkMark:.none
return cell
}
}
extension CreateEventStep2VC : UITableViewDelegate {
//MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
if selectedEventTypes.contains(indexPath) {
selectedEventTypes.remove(indexPath)
} else if selectedEventTypes.count < 3 {
selectedEventTypes.insert(indexPath)
}
tableView.reloadRows(at: [indexPath], animated:.none)
}
}
You can have array of indexPath rows allArr like this
1- when user selects more than 3 the first one will be automatically dropped
var allArr = [Int]()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .checkmark
allArr.append(indexPath.row)
}
if(allArr.count == 4)
{
allArr.dropFirst()
}
}
2- add this to cellForRow
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EventTypeCell", for: indexPath) as! CreateEventStep2Cell
cell.eventTypeNames = eventTypeAvailableData[indexPath.row]
if allArr.contains(indexPath.row) {
cell.accessoryType = .checkmark
}
else
{
cell.accessoryType = .none
}
return cell
}
3- remove code in didSelectRowAt
I am making a music genre picking application and when I go to my table to select genres, I select a row and it selects a random row about 10 or so down from my selection.
My code for the selection is:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let genresFromLibrary = genrequery.collections
let rowitem = genresFromLibrary![indexPath.row].representativeItem
print(rowitem?.value(forProperty: MPMediaItemPropertyGenre) as! String
)
if let cell = tableView.cellForRow(at: indexPath)
{
cell.accessoryType = .checkmark
}
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath)
{
cell.accessoryType = .none
}
}
Cells are reused by default when cellForRowAtIndexPath is called. This causes the cells to have the wrong data when you don't keep track of the indexPaths that have been selected. You need to keep track of the index paths that are currently selected so you can show the appropriate accessory type in your table view.
One way of doing it is to have a property in your UITableViewController that just stores the index paths of the selected cells. It can be an array or a set.
var selectedIndexPaths = Set<IndexPath>()
When you select a row on didSelectRowAt, add or remove the cell from selectedIndexPaths, depending on whether the index path is already in the array or not:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedIndexPaths.contains(indexPath) {
// The index path is already in the array, so remove it.
selectedIndexPaths.remove(indexPathIndex)
} else {
// The index path is not part of the array
selectedIndexPaths.append(indexPath)
}
// Show the changes in the selected cell (otherwise you wouldn't see the checkmark or lack thereof until cellForRowAt got called again for this cell).
tableView.reloadRows(at: [indexPath], with: .none)
}
Once you have this, on your cellForRowAtIndexPath, check if the indexPath is in the selectedIndexPaths array to choose the accessoryType.
if selectedIndexPaths.contains(indexPath) {
// Cell is selected
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
This should solve the problem of the seemingly random cells that are checked every 10 cells down or so (which, is not random, it's just that the cell with the checkmark is being reused).
Because cellForRow returns a cached cell you generated. When scrolling out of the screen the order of cells are changed and cells are reused. So it seems "randomly selected".
Don use cellForRow, instead record selection data.
Here's code works in a single view playground.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
var selection: [IndexPath: Bool] = [:]
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.tableFooterView = UIView()
view.addSubview(tableView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = self.view.bounds
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "c")
if let sc = cell {
sc.accessoryType = .none
let isSelected = selection[indexPath] ?? false
sc.accessoryType = isSelected ? .checkmark : .none
return sc
}
return UITableViewCell(style: .default, reuseIdentifier: "c")
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.textLabel?.text = NSNumber(value: indexPath.row).stringValue
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selection[indexPath] = true
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I'm quite new to Swift and I would like to know what is the easiest and simplest way for me to add in 10 buttons to 10 of my cell rows in my TableViewController.
P.S: It would be nice if the 10 buttons perform differently instead of duplicate.
import UIKit
import FirebaseDatabase
var ref: DatabaseReference?
var databaseHandle: DatabaseHandle?
var postData = [String]()
class TableViewController5: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference() //set the firebase reference
// Retrieve the post and listen for changes
databaseHandle = ref?.child("Posts3").observe(.value, with: { (snapshot) in
postData.removeAll()
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
postData.append(key)
}
self.tableView.reloadData()
})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.font = UIFont.init(name: "Helvetica", size: 23)
cell.textLabel?.text = postData[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 80
}
}
From what I can understand, you should use prototype cell and add an IBAction in the cell and in that cell you should perform the segue and pass whatever data you need to customise the page you load.
In your tableView(_:cellForRowAt:) method, you can set a tag for the button.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? prototypeCell
cell.voteButton.tag = indexPath.row
return cell
}
Then in your cell's class, in the IBAction I mentioned earlier, check for the tag and set the data to pass accordingly.
Then in your prepareForSegue:sender: method, just pass the data you want to pass to the next view controller and all should work fine.
Check if it helps You
//Common cell Variable
var cell = UITableViewCell()
//UIButton commonly Declared
var acceptRequest = UIButton()
var declineRequest = UIButton()
My TableView Cell contains two button as shown Accept & Decline
In my tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) I added Handlers for button
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Cell for dequeuing
cell = self.RequestListTableView.dequeueReusableCell(withIdentifier: "Cell")!
//Used Tags here
//You can Make use of TableViewCell class here and Access Buttons from there
acceptRequest = cell.viewWithTag(4) as! UIButton
//Adding action Handlers for UIbuttons
self.acceptRequest.addTarget(self, action: #selector(RequestListView.AcceptRequest(sender:)), for: .touchUpInside)
return cell
}
my Button handler
func AcceptRequest (sender: UIButton) {
//Dequeue cell with identifier can be ignored
cell = self.RequestListTableView.dequeueReusableCell(withIdentifier: "Cell")!
//Get button position from TableView : Required
let buttonPosition : CGPoint = sender.convert(CGPoint.zero, to: self.RequestListTableView)
//Get index Path of selected Cell from TableView
//Returns Table Cell index same as didSelect
let indexPath : IndexPath = self.RequestListTableView.indexPathForRow(at: buttonPosition)!
//Can be used if you need to update a view
cell = self.RequestListTableView.cellForRow(at: indexPath)!
//Now time to access using index Path
cell.textLabel.text = array[indexPath.row] //Example
}
Can use this Procedure for even single Button or for multiple buttons too
In case if you have more than one button in one cell, this will help you
I need to detect if the button has been clicked in the UITableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let LikesBtn = cell.viewWithTag(7) as! UIButton
}
The easiest and most efficient way in Swift is a callback closure.
Subclass UITableViewCell, the viewWithTag way to identify UI elements is outdated.
Set the class of the custom cell to the name of the subclass and set the identifier to ButtonCellIdentifier in Interface Builder.
Add a callback property.
Add an action and connect the button to the action.
class ButtonCell: UITableViewCell {
var callback : (() -> Void)?
#IBAction func buttonPressed(_ sender : UIButton) {
callback?()
}
}
In cellForRow assign the callback to the custom cell.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCellIdentifier", for: indexPath) as! ButtonCell
cell.callback = {
print("Button pressed", indexPath)
}
return cell
}
When the button is pressed the callback is called. The index path is captured.
Edit
There is a caveat if cells can be added or removed. In this case pass the UITableViewCell instance as parameter and get the index path from there
class ButtonCell: UITableViewCell {
var callback : ((UITableViewCell) -> Void)?
#IBAction func buttonPressed(_ sender : UIButton) {
callback?(self)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCellIdentifier", for: indexPath) as! ButtonCell
let item = dataSourceArray[indexPath.row]
// do something with item
cell.callback = { cell in
let actualIndexPath = tableView.indexPath(for: cell)!
print("Button pressed", actualIndexPath)
}
return cell
}
If even the section can change, well, then protocol/delegate may be more efficient.
First step:
Make Subclass for your custom UITableViewCell, also register protocol.
Something like this:
protocol MyTableViewCellDelegate: class {
func onButtonPressed(_ sender: UIButton, indexPath: IndexPath)
}
class MyTableViewCell: UITableViewCell {
#IBOutlet var cellButton: UIButton!
var cellIndexPath: IndexPath!
weak var delegate: MyTableViewCellDelegate!
override func awakeFromNib() {
super.awakeFromNib()
cellButton.addTarget(self, action: #selector(self.onButton(_:)), for: .touchUpInside)
}
func onButton(_ sender: UIButton) {
delegate.onButtonPressed(sender, indexPath: cellIndexPath)
}
}
In your TableViewController, make sure it conform to your just created protocol "MyTableViewCellDelegate".
Look at the code below for better understanding.
class MyTableViewController: UITableViewController, MyTableViewCellDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as? MyTableViewCell {
cell.cellIndexPath = indexPath
cell.delegate = self
return cell
} else {
print("Something wrong. Check your cell idetifier or cell subclass")
return UITableViewCell()
}
}
func onButtonPressed(_ sender: UIButton, indexPath: IndexPath) {
print("DID PRESSED BUTTON WITH TAG = \(sender.tag) AT INDEX PATH = \(indexPath)")
}
}
Here is what I use:
First initialize the button as an Outlet and its action on your TableViewCell
class MainViewCell: UITableViewCell {
#IBOutlet weak var testButton: UIButton!
#IBAction func testBClicked(_ sender: UIButton) {
let tag = sender.tag //with this you can get which button was clicked
}
}
Then in you Main Controller in the cellForRow function just initialize the tag of the button like this:
class MainController: UIViewController, UITableViewDelegate, UITableViewDataSource, {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! MainViewCell
cell.testButton.tag = indexPath.row
return cell
}
}
I am making a simple client invoice list using a tableView with custom labels in each row. at the top you click the plus sign. This takes you to a second page where you type in your clients name. you hit save and it should add it to the label in the first view controller. I cannot get this to work.
my tableview storyboard has the file called ViewController.swift - in it
import UIKit
var invoiceList = [""]
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var clientTableView: UITableView!
#IBOutlet var clientLabel: UILabel!
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return invoiceList.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "clientItem", for: indexPath) as! UITableViewCell
let ClientName = invoiceList[indexPath.row]
cell.textLabel?.text = [clientInput]
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete
{
invoiceList.remove(at: indexPath.row)
clientTableView.reloadData()
}
}
override func viewDidAppear(_ animated: Bool) {
clientTableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
My second storyboard where you type in the text field. File is called AddInvoiceItem.swift. in it -
import UIKit
class AddInvoiceViewController: UIViewController {
var clientTextField:String!
#IBOutlet var clientInput: UITextField!
#IBAction func addItem(_ sender: Any) {
if clientInput.text != ""
{
invoiceList.append(clientInput.text!)
clientInput.text = ""
_ = navigationController?.popViewController(animated: true)
}
}
#IBAction func cancelBtn(_ sender: Any) {
_ = navigationController?.popViewController(animated: true)
}
}
My error keeps happening when I am trying to use the code line
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "clientItem", for: indexPath) as! UITableViewCell
let ClientName = invoiceList[indexPath.row]
cell.textLabel?.text = [clientInput]
return cell
}
The cell.textLabel?.text = [clientInput]
I know this is wrong, but cannot figure it out!
Make a new cell class
class CustomCell: UITableViewCell {
//Make your outlets here, connect the outlets from cell in your storyboard like ClientNameLabel
}
Please select the cell on your Storyboard, and enter the name of your class in this field. In your case it would be CustomCell
then in ViewController
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "clientItem", for: indexPath) as! CustomCell
let ClientName = invoiceList[indexPath.row]
cell.ClientNameLabel.text = ClientName
return cell
}