Swift 3 button.addTarget not working on all table cells - ios

I have created an app to allow users to store various voice recordings against reviews. When I display this a table and the data is populated with the following code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = (indexPath as NSIndexPath).item
let cell = tableView.dequeueReusableCell(withIdentifier: "voiceRecordingCell", for: indexPath) as! VoiceRecordingTableViewCell
let voiceRecording = self.voiceRecordings[row] as! NSDictionary
let isoFormatter = DateFormatter()
isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'.000Z'"
let createdAt = isoFormatter.date(from: voiceRecording["created_at"] as! String)
self.recordingIndexPaths.insert(indexPath, at: row)
cell.recording = voiceRecording
cell.date.text = getDateFormatter("dd-MM-y").string(from: createdAt!)
cell.time.text = getDateFormatter("HH:mm").string(from: createdAt!)
cell.length.text = voiceRecording["length"] as? String
cell.location.text = voiceRecording["location"] as? String
let audioPlayerController = self.storyboard?.instantiateViewController(withIdentifier: "AudioPlayerController") as! AudioPlayerController
audioPlayerController.voiceRecording = voiceRecording
cell.audioPlayer.addSubview(audioPlayerController.view)
self.addChildViewController(audioPlayerController)
audioPlayerController.didMove(toParentViewController: self)
cell.deleteRecordingButton.tag = row
cell.deleteRecordingButton.addTarget(self, action: #selector(deleteRecordingPressed(_:)), for: .touchUpInside)
return cell
}
The cells all appear to be rendering correctly however for the cells that are not initially rendered with the page, the ones I have to scroll down to view, when I click on the buttons either on the audio player controls or the deleteRecordingButton nothing happens, its as though the addTarget is not being set. The code to set the buttons is being called and doesn't create an error, its just not applying to those button.
The buttons that are initially displayed on the screen have the correct actions and all work perfectly so I know that works.
I'm really at a loss as to what is causing this. I did try searching google and stackoverflow but I've not found anything. Any assistance with this would be greatly received.
--------------- UPDATE -----------
I just tried putting some breakpoints in
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
That also only works get called on the top 2 cells or the top one if in landscape!

Since the cell gets reused all the time the reference get lost.
Try something like this:
class ButtonTableViewCell: UITableViewCell {
typealias TapClosure = () -> Void
var buttonTapped: TapClosure?
#IBAction func buttonTouchUpInside(sender: UIButton) {
buttonTapped?()
}
}
extension TestViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! ButtonTableViewCell
cell.buttonTapped = {
print("Button tapped")
}
return cell
}
}
And another tipp. Never init an DateFormatter in cellForRowAtIndexPath. Instead create it in viewDidLoad or in a static struct for reuse.

Finally I have figured this out...
This was happening because I was using a scrollview with to scroll up and down the table without using the tables native scroll functionality.
When using the tables scroll functionality the buttons are applied the actions as they are brought into the view. This is handled by the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { method. However when you do it the way I was it initially renders all the cells and the ones that are off the screen don't work!
Its a little bit annoying working in this way but at least I now know about it.

Related

How to configure a button containing an image in TableViewCell.swift file, and implement showing different images in each table view cell?

I am trying to show a button as an image of restaurant food and a button below that as a text label containing the name of that restaurant all in a custom table view cell. So when you scroll down the table view, you see pictures of different restaurant food and the name of the restaurant below the picture. I have a XIB file I’m using for this.
At the moment, I’m using a UIImage for the pictures of the restaurants, and this is working, but I’m trying to instead use a button with an image, so that I can make an #IBAction after the picture of the food that is a button is clicked.
I have configured a button showing restaurant names in the TableViewCell.swift file as shown below (the labels show when ran):
TableViewCell.swift code:
#IBOutlet var labelButton: UIButton!
//For retaining title in a property for IBAction use.
private var title: String = ""
func configureLabelButton(with title: String) {
self.title = title //For retaining title in a property for IBAction use.
labelButton.setTitle(title, for: .normal)
}
and implemented showing different restaurant names in each table view cell from an array with the restaurant names as strings in the view controller as shown below.
ViewController.swift code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", for: indexPath) as! RestaurantTableViewCell
cell.configureLabelButton(with: myData[indexPath.row])
return cell
}
I am trying to do the same thing for showing an image in a button. I have an array of images I'm using for this same purpose called myImages. Here is the code I have so far, but it is not working when run.
TableViewCell.swift code:
#IBOutlet var imageButton: UIButton!
//For retaining image in a property for IBAction use.
private var image: UIImage = UIImage()
func configureImageButton(with image: UIImage) {
self.image = image //For retaining image in a property for IBAction use.
imageButton.setImage(image, for: .normal)
}
ViewController.swift code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", for: indexPath) as! RestaurantTableViewCell
cell.configureImageButton(with: myImages[indexPath.row]!)
cell.imageButton.contentMode = .scaleAspectFill
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 185
}
I think my error is somewhere in TableViewCell.swift. I think I do not have the correct code there and potentially in ViewController.swift.
I think there is a problem in the UIImage array declaration, the issue might be it not getting an image from the array.
try with this one
var arrImg : [UIImage] = [UIImage(named: "res-1")!,UIImage(named: "res-2")!,UIImage(named: "res-3")!]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", for: indexPath) as! RestaurantTableViewCell
cell.configureImageButton(with: arrImg[indexPath.row])
cell.btnResturant.contentMode = .scaleAspectFill
cell.configureLabel(with: arrRes[indexPath.row])
cell.layoutIfNeeded()
cell.selectionStyle = .none
cell.layoutIfNeeded()
return cell
}
Found the answer to my question. The syntax in TableView.swift was wrong. This may have been due to a Swift update.
Was:
imageButton.setImage(image, for: .normal)
Should be:
imageButton.setBackgroundImage(image, for: .normal)

Sorting custom table view cells in Swift

I am working on an small project where I have an app that takes in tvshow information entered by the user and displays it in a custom tableview cell. I would like to sort the shows as they are entered based on which current episode the user is on. I know this code works because I tested it with print statements and it sorts the array but it does not sort on the simulator. So I just was curious where I should place this so that it sorts on the app side.
func sortShows() {
let sortedShows = tvShows.sorted { $0.currentEpisode > $1.currentEpisode}
TVShowTableView.reloadData()
print(sortedShows)
}
Here is where I am currently placing it inside my view controller
extension TVShowListViewController: AddTVShowDelegate {
func tvShowWasCreated(tvShow: TVShow) {
tvShows.append(tvShow)
dismiss(animated: true, completion: nil)
TVShowTableView.reloadData()
sortShows()
}
}
In this part of your code:
func sortShows() {
// here you are creating a NEW array
let sortedShows = tvShows.sorted { $0.currentEpisode > $1.currentEpisode}
// here you tell the table view to reload with the OLD array
TVShowTableView.reloadData()
print(sortedShows)
}
In your controller class, you probably have something like:
var tvShows: [TVShow] = [TVShow]()
and then you populate it with shows, like you do with a new show:
tvShows.append(tvShow)
Then your controller is doing something like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tvShowCell", for: indexPath) as! TVShowCell
cell.tvShow = tvShows[indexPath.row]
return cell
}
What you want to do is add another var to your class:
var sortedShows: [TVShow] = [TVShow]()
then change your sort func to use that array:
func sortShows() {
// use the existing class-level array
sortedShows = tvShows.sorted { $0.currentEpisode > $1.currentEpisode}
// here you tell the table view to reload
TVShowTableView.reloadData()
print(sortedShows)
}
and change your other funcs to use the sortedShows array:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// use sortedShows array
return sortedShows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tvShowCell", for: indexPath) as! TVShowCell
// use sortedShows array
cell.tvShow = sortedShows[indexPath.row]
return cell
}
and you'll want to call sortShows() at the end of viewDidLoad() (or wherever you are getting your initial list of shows).
Edit
Another way you might use cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tvShowCell", for: indexPath) as! TVShowCell
// use sortedShows array
let tvShow = sortedShows[indexPath.row]
cell.showTitleLable.text = tvShow.title
cell.showDecriptionLable.text = tvShow.description
return cell
}

Making button clickable UITableView Xcode

I made a custom tableview cell with a nib file in my project with a button. How do I make it so that when the button is clicked, I can execute a simple task, like printing hi? I saw other posts that are similar, but don't completely understand them.
Thanks in Advance....
You should add target to your button like this :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "ShipmentDeliveryFactorTableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! ShipmentDeliveryFactorTableViewCell
let object = factorArray[indexPath.row]
cell.detailButton1.addTarget(self, action: #selector(showDetail(_:)), for: .touchUpInside)
cell.detailButton1.tag = indexPath.row
return cell
}
#objc func showDetail(_ sender: UIButton) {
let selectedCell = factorArray[sender.tag]
print("selectedCell.id")
}

Iterating through TableViewCells skips some

I have a tableview and in each cell there is a checkbox. I also have a "select all" button.
My problem is that when I click select all I want to update all the checkboxes to checked state. So from a list of 100 cells, all get checked but every 13th cell does not. To make it clearer, on my simulators screen are 12 cells visible that all get checked. When I start scrolling, the first cell that comes up is unchecked, and is then followed by 12 checked ones :S
When I scroll a little and click "select all" again, the skipped ones become also checked..
Anyone have a clue what am I missing?
This is the cell code:
class ListTableViewCell: UITableViewCell {
#IBOutlet weak var checkbox: UIButton!
var buttonState = false{
didSet{
if buttonState{
checkbox.setImage(#imageLiteral(resourceName: "checked"), for: .normal)
}else{
checkbox.setImage(#imageLiteral(resourceName: "unchecked"), for: .normal)
}
}
}
#IBAction func checkboxAction(_ sender: UIButton) {
if buttonState {
buttonState = false
}else{
buttonState = true
}
}
func simulateCheck(){
buttonState = true
}
And here are some snipets from my controller:
private var articleValues: [ArticleValue] = []{
didSet{
tableView.reloadData()
}
}
func selectAll(){
for i in 0..<articleValues.count{
let cell = tableView.cellForRow(at: IndexPath(item: i, section: 0)) as? ListTableViewCell
cell?.simulateCheck()
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
}
return cell
}
Your UITableView is backed by a data source. This means that you shouldn't change cells directly like you do here:
cell?.simulateCheck()
tableView.reloadData()
Instead you should keep a list of all the checked positions, maybe another array that has bools for each corresponding articleValue (this is not the best design).
var checkedValues = Bool
In your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method you would then set the state of the cell:
articleValueCell.buttonState = checkedValues[indexPath.row]
In your selectAll method fill this array with true values and then call tableView.reloadData()
private var checkedValues = [Bool]()
private var articleValues: [ArticleValue] = []{
didSet{
checkedValues = Array(repeating: false, count: articleValues.count)
tableView.reloadData()
}
}
func selectAll(){
checkedValues = Array(repeating: true, count: articleValues.count)
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
articleValueCell.buttonState = checkedValues[indexPath.row]
}
return cell
}
Another mistake is that you should never iterate on all the cells in the table because they are reused, no point in going through your data source and getting a cell for each. It only makes sense to iterate through tableView.visibleCells. But like in your case, most of the time you don't need that either, you should just update your data source accordingly and reload the table or just the modified cell.
It's not recommended that you refer to cells directly within a table view. The reason is that UITableViews have an efficient method of only loading the cells as they are needed (and deallocating them when they are no longer needed, e.g. the cell scrolls off screen). Because of this the cell you are try to refer to may not be loaded.
Instead you should interact with it via the cellForRowAt method. If you want to "select all" cells, you should create a property that stores the value of checked or not checked via a Bool and then set all of the ArticleValue elements to true for that property and reload the data inside selectAll().
It could work something like this:
func selectAll() {
articleValues.forEach {
$0.checked = true
}
tableView.reloadData()
}
// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
if articleValue.checked {
articleValueCell.simulateCheck()
}
}
return cell
}

How to add a "Load More" cell that when pressed adds more rows to the tableview?

I'm trying to create a tableview only initially shows 5 cells plus a "load more" cell when is pressed more cells will be presented. I'm downloading JSON data and splitting it into 3 arrays, one for the total data, one for data for only the first 5 cells and one for the remaining data after the first 5 cells' data is subtracted from the total data.
I saw this question on here: Load More Cells and tried implementing what they did but with little success. This is what we have so far -
pretty simple, straightforward storyboard with 2 different cells, the first one is the "test cell" which contains the JSON data, the second one is the "expand cell" that when pressed is supposed to show the rest of the cells. Here is my code so far, I omitted a lot of it to keep this post short, I'm currently getting an error it's marked:
var castArray: [CastData?]? = []
var first5CastArray: [CastData?]? = []
var remainderCastArray: [CastData?]? = []
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let firstArray = first5CastArray {
return firstArray.count + 1
} else {
return 0
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row < (first5CastArray?.count)! {
let testCell = tableView.dequeueReusableCell(withIdentifier: "testCell", for: indexPath) as! TestCell
testCell.castImageView.image = first5CastImageArray?[indexPath.row]
return testCell
} else {
let expandCell = tableView.dequeueReusableCell(withIdentifier: "expandCell", for: indexPath) as! ExpandCell
expandCell.moreLabel.text = "More"//Error here -fatal error: unexpectedly found nil while unwrapping an Optional value
return expandCell
}
}
//Below code never tested because of fatal error from cellForRowAt indexPath!!!!!
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let remainderArray = remainderCastArray {
for info in remainderArray {
first5CastArray?.append(info)
}
}
expandCell.isHidden = true
tableview.reload
}
I'm not completely sure if I'm going about this the right way, any help is appreciated!
Go back to your storyboard, select that cell again, go to "Attributes Inspector" tab, type in expandCell in identifier field.

Resources