I have a table view with expanding cells. The expanding cells come from a xib file. In the class of the table is where all of the code is that controls the expansion and pulling data from plist. I'm trying to add a close button but only want it to show when the cell is expanded. As it stands, I can't reference the button to hide it because it's in another class. Here is how I am trying to access it:
import UIKit
class SecondPolandViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var customTableViewCell:CustomTableViewCell? = nil
var items = [[String:String]]()
override func viewDidLoad() {
super.viewDidLoad()
**REFERENCING CLASS**
customTableViewCell = CustomTableViewCell()
let nib = UINib.init(nibName: "CustomTableViewCell", bundle: nil)
self.tableView.register(nib, forCellReuseIdentifier: "cell")
self.items = loadPlist()
}
func loadPlist()->[[String:String]]{
let path = Bundle.main.path(forResource: "PolandResourceList", ofType: "plist")
return NSArray.init(contentsOf: URL.init(fileURLWithPath: path!)) as! [[String:String]]
}
var selectedIndex:IndexPath?
var isExpanded = false
func didExpandCell(){
self.isExpanded = !isExpanded
self.tableView.reloadRows(at: [selectedIndex!], with: .automatic)
}
}
extension SecondPolandViewController:UITableViewDataSource, UITableViewDelegate{
***HIDING BUTTON***
let button = customTableViewCell?.closeButton
button?.isHidden = true
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.selectedIndex = indexPath
self.didExpandCell()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomTableViewCell
cell.selectionStyle = .none
let item = self.items[indexPath.row]
cell.titleLabel.text = item["title"]
cell.shortLabel.text = item["short"]
cell.otherImage.image = UIImage.init(named: item["image"]!)
cell.thumbImage.image = UIImage.init(named: item["image"]!)
cell.longLabel.text = item["long"]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let height = UIScreen.main.bounds.height
if isExpanded && self.selectedIndex == indexPath{
//return self.view.frame.size.height * 0.6
return 400
}
return 110
//return height * 0.2
}
}
This does not hide it though.
Here is the xib that I am calling from if it helps. It is probably simple, I am just a newly self taught developer.
import UIKit
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var closeButton: UIImageView!
#IBOutlet weak var otherImage: UIImageView!
#IBOutlet weak var thumbImage: UIImageView!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var shortLabel: UILabel!
//#IBOutlet weak var longLabel: UITextView!
#IBOutlet weak var longLabel: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
//let width = UIScreen.main.bounds.width
//let height = UIScreen.main.bounds.height
//thumbImage.frame.size.width = height * 0.19
//thumbImage.frame.size.height = height * 0.19
}
}
It seems like that you just need to add these lines into cellForRowAt:indexPath method:
if indexPath == selectedIndexPath {
cell.closeButton.isHidden = false
} else {
cell.closeButton.isHidden = true
}
You may add them right before return line
The normal iOS answer for this is a delegate, but you could get away with a simple closure in this case.
In CustomTableViewCell, add
public var closeTapped: ((CustomTableViewCell) -> ())?
Then in that class, when close is tapped, call
self.closeTapped?(self)
In the VC, in cellForRowAt,
cell.closeTapped = { cell in
// do what you want with the VC
}
For delegates, this might help: https://medium.com/#jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef
The quick answer to why to prefer delegates over the closure is that its a handy way to group a bunch of these together. It's what UITableViewDelegate is (which you are using). Also, it's a common iOS idiom.
I wrote about this here: https://app-o-mat.com/post/how-to-pass-data-back-to-presenter for a similar situation (VC to VC communication)
Related
How can I properly trigger a view controller to open when a UITableView row is pressed?
I need to allow the user to go back from the view controller back to the tableView and allow them to select the same or a different tableView row.
The problem I am currently having is the application crashes when selecting the same row more than once after returning back from ViewController that opens when selecting on one of the rows: scheduledDelivery
Currently, this is the code I have:
import UIKit
class ScheduledCell: UITableViewCell {
#IBOutlet weak var ETALabel: UILabel!
#IBOutlet weak var cellStructure: UIView!
#IBOutlet weak var scheduledLabel: UILabel!
#IBOutlet weak var testingCell: UILabel!
#IBOutlet weak var pickupLabel: UILabel!
#IBOutlet weak var deliveryLabel: UILabel!
#IBOutlet weak var stopLabel: UILabel!
#IBOutlet weak var topBar: UIView!
}
class ToCustomerTableViewController: UITableViewController, UIGestureRecognizerDelegate {
var typeValue = String()
var driverName = UserDefaults.standard.string(forKey: "name")!
var structure = [AlreadyScheduledStructure]()
override func viewDidLoad() {
super.viewDidLoad()
fetchJSON()
//Disable delay in button tap
self.tableView.delaysContentTouches = false
tableView.tableFooterView = UIView()
}
private func fetchJSON() {
guard let url = URL(string: "https://example.com/example/example"),
let value = driverName.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "driverName=\(value)".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
self.structure = try JSONDecoder().decode([AlreadyScheduledStructure].self,from:data)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print(error)
}
}.resume()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return structure.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "scheduledID", for: indexPath) as! ScheduledCell
let portfolio = structure[indexPath.row]
cell.stopLabel.text = "Stop \(portfolio.stop_sequence)"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let portfolio = structure[indexPath.row]
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "scheduledDelivery")
print(portfolio.customer)
let navTitle = portfolio.customer
UserDefaults.standard.set(navTitle, forKey: "pressedScheduled")
controller.navigationItem.title = navTitle
navigationController?.pushViewController(controller, animated: true)
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 200.0
}
}
Notice how in cellForRowAt I am setting the cell as a dequeueReusableCell which might be why the app is crashing sometimes when selecting the same cell more than once
let cell = tableView.dequeueReusableCell(withIdentifier: "scheduledID"
I have also noticed that if the tableView rows are reloaded on viewDidAppear it does not crash as often, but of course, this is a terrible solution.
Error I get:
'NSInternalInconsistencyException', reason: 'Attempted to dequeue
multiple cells for the same index path, which is not allowed. If you
really need to dequeue more cells than the table view is requesting,
use the -dequeueReusableCellWithIdentifier: method
According to the crash replace
let cell = tableView.dequeueReusableCell(withIdentifier: "scheduledID", for: indexPath) as! ScheduledCell
with
let cell = tableView.dequeueReusableCell(withIdentifier: "scheduledID") as! ScheduledCell
There is a task. Each cell contains a button by clicking which you want to delete this cell. The problem is that sections are used to delineate the entire list by category. The data I take from Realm DB. removal must occur under two conditions because the name is repeated, so you need to consider the name from the label and the name of the section. I will be very grateful for the sample code with comments.
import UIKit
import RealmSwift
class PurchesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var purchesTableView: UITableView!
let manage = ManagerData()
override func viewDidLoad() {
super.viewDidLoad()
purchesTableView.delegate = self
purchesTableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
purchesTableView.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return manage.loadPurchases().0.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return manage.loadPurchases().0[section]
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return manage.loadPurchases().1[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "purchesCell", for: indexPath) as! CustomPurchesTableViewCell
cell.productLabel.text = manage.loadPurchases().1[indexPath.section][indexPath.row]
cell.weightProductLabel.text = manage.loadPurchases().2[indexPath.section][indexPath.row]
cell.weightNameLabel.text = manage.loadPurchases().3[indexPath.section][indexPath.row]
// cell.boughtButton.addTarget(self, action: #selector(removeProduct), for: .touchUpInside)
return cell
}
}
class CustomPurchesTableViewCell: UITableViewCell {
#IBOutlet weak var boughtButton: UIButton!
#IBOutlet weak var productLabel: UILabel!
#IBOutlet weak var weightProductLabel: UILabel!
#IBOutlet weak var weightNameLabel: UILabel!
#IBAction func removePurches(_ sender: Any) {
print("remove")
}
}
method for get data
func loadPurchases() -> ([String], Array<Array<String>>, Array<Array<String>>, Array<Array<String>>) {
var sections: [String] = []
var product = Array<Array<String>>()
var weight = Array<Array<String>>()
var nameWeight = Array<Array<String>>()
let realm = try! Realm()
let data = realm.objects(Purches.self)
for item in data {
if sections.contains(item.nameDish) == false {
sections.append(item.nameDish)
}
}
for a in sections {
var productArr = Array<String>()
var weightArr = Array<String>()
var nameWeightArr = Array<String>()
for prod in data {
if a == prod.nameDish {
productArr.append(prod.product)
weightArr.append(prod.weight)
nameWeightArr.append(prod.nameWeigh)
}
}
product.append(productArr)
weight.append(weightArr)
nameWeight.append(nameWeightArr)
}
return (sections, product, weight, nameWeight)
}
Index path you will get in cell class
Index path have two property section and row for table view
Now you can create on more method in Controller class and assign to a variable to every cell or you can use editAction provided by table view for delete
in order to get number section and row you need create IBOutlet in custom cell and on ViewController class is created addTarget for your button.
Example code at the bottom.
import UIKit
import RealmSwift
class PurchesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var purchesTableView: UITableView!
let manage = ManagerData()
//... more code ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "purchesCell", for: indexPath) as! CustomPurchesTableViewCell
cell.productLabel.text = manage.loadPurchases().1[indexPath.section][indexPath.row]
cell.weightProductLabel.text = manage.loadPurchases().2[indexPath.section][indexPath.row]
cell.weightNameLabel.text = manage.loadPurchases().3[indexPath.section][indexPath.row]
cell.boughtButton.addTarget(self, action: #selector(removePurches(_:)), for: .touchUpInside)
return cell
}
#objc func removePurches(_ sender: UIButton) {
let position: CGPoint = sender.convert(CGPoint.zero, to: purchesTableView)
let indexPath: IndexPath! = self.purchesTableView.indexPathForRow(at: position)
print("indexPath.row is = \(indexPath.row) && indexPath.section is = \(indexPath.section)")
purchesTableView.deleteRows(at: [indexPath], with: .fade)
}
}
and custom class CustomPurchesTableViewCell for cell
class CustomPurchesTableViewCell: UITableViewCell {
#IBOutlet weak var boughtButton: UIButton! // you button for press
#IBOutlet weak var productLabel: UILabel!
#IBOutlet weak var weightProductLabel: UILabel!
#IBOutlet weak var weightNameLabel: UILabel!
}
I'm trying to get the selected row in a table in Swift 4. The code presented for completeness, is as follows:
import UIKit
import WebKit
class FactorDetailsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var goalTitle: UILabel!
#IBOutlet weak var goalCopy: UITextView!
#IBOutlet weak var goalBenefit: UILabel!
#IBOutlet weak var goalName: UILabel!
#IBOutlet weak var graph: SimpleChart!
#IBOutlet weak var measurement: UILabel!
#IBOutlet weak var measurementRange: UILabel!
#IBOutlet weak var updated: UILabel!
#IBOutlet weak var factorCopy: UITextView!
#IBOutlet weak var impact: UILabel!
#IBOutlet weak var actionsTable: UITableView!
#IBOutlet weak var researchTable: UITableView!
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var stackView: UIStackView!
var factorData : Factor?
var currentCategory : FactorCategory?
var recommendedActions : [Action] = []
var relatedResearch : [Research] = []
var goal : Goal?
var reading:Double?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = factorData?.factorName
measurement.text = String(reading!)
self.actionsTable.delegate = self
self.actionsTable.dataSource = self
self.actionsTable.isEditing = false
self.researchTable.delegate = self
self.researchTable.dataSource = self
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Update", style: .done, target: self, action: #selector(addStuff))
goalCopy.text = goal?.copy
goalBenefit.text = "BIG BENEFIT"
goalTitle.text = goal?.title
for cat in (factorData?.categories)! {
let s = SimpleChartData(min : Double(cat.min), max : Double(cat.max), label : cat.label, label2 : String(cat.max))
if( graph.canLoad ) {
graph.data!.append(s)
}
}
graph.reading = reading!
// Pin the edges of the stack view to the edges of the scroll view that contains it
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
}
#objc func addStuff() {
// how does this work? Just takes you to the quiz again
let storyboard = UIStoryboard(name: "12Factor", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "AllQuestionViewController") as UIViewController
present(vc, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func configure( factor : Factor )
{
factorData = factor
let mirrored_object = Mirror(reflecting: HRResponses.shared)
reading = 1.0
for (_, attr) in mirrored_object.children.enumerated() {
if let property_name = attr.label as String? {
if factorData?.responseField == property_name {
if let a = attr.value as? String, let aDouble = Double(a) {
reading = aDouble
}
}
}
}
currentCategory = factor.categories.first( where: {$0.min < reading! && $0.max > reading! })
for aind in (currentCategory?.actions)! {
let act = SignalModel.model.actions.first( where: {$0.id == aind})
recommendedActions.append(act!)
}
for rind in (currentCategory?.research)! {
let act = SignalModel.model.research.first( where: {$0.id == rind})
relatedResearch.append(act!)
}
goal = SignalModel.model.goals.first( where: {$0.id == currentCategory?.goals[0]})
}
func tableView(_ tableView: UITableView,
willSelectRowAt indexPath: IndexPath) -> IndexPath? {
print("works?")
return nil
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
if tableView == self.actionsTable {
let vc : ActionViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ActionViewController") as! ActionViewController
navigationController?.pushViewController(vc, animated: true)
}
if tableView == self.researchTable {
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of items in the sample data structure.
var count:Int?
if tableView == self.actionsTable {
count = currentCategory?.actions.count
}
if tableView == self.researchTable {
count = currentCategory?.research.count
}
return count!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell:UITableViewCell?
if tableView == self.actionsTable {
cell = tableView.dequeueReusableCell(withIdentifier: "ActionCell", for: indexPath as IndexPath)
let action = recommendedActions[indexPath.row]
(cell as! ActionCell).configure(a:action)
}
if tableView == self.researchTable {
cell = tableView.dequeueReusableCell(withIdentifier: "ResearchCell", for: indexPath as IndexPath)
let research = relatedResearch[indexPath.row]
(cell as! ResearchCell).configure(r:research)
}
return cell!
}
}
Now, that's too much code. The relevant parts are here:
self.actionsTable.delegate = self // yes, this is the delegate
self.actionsTable.dataSource = self
self.actionsTable.isEditing = false // no, we're not editing
As I understand it, this should be enough to have selections in the actionsTable trigger the
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
method. However, nothing happens. The other UITableViewDelegate methods are called, so this controller is the delegate for this table, however this one method is not ever triggered. Reading through the Apple documentation here I see that the method isn’t called when the table view is in editing mode (that is, the isEditing property of the table view is set to true), but my table isn't in editing mode. Is there something else that could be going wrong with my table that wouldn't allow it to send an event to a UITableViewDelegate? I suspect that this has something to do with the table being inside a UIScrollView, which I've read isn't best practice, but with the design I've been given, is non-negotiable sadly.
Your problem is not the delegate, all of that code is good. Your problem is that the parent scroll view is consuming the taps, not the table view. Remember, UITableView is a direct subclass of UIScrollView so placing a table view inside a scroll view is no different than placing a scroll view within a scroll view. UITableView has all of the default scroll view delegates built into it so just use those.
You should not embed UIWebView or UITableView objects in UIScrollView
objects. If you do so, unexpected behavior can result because touch
events for the two objects can be mixed up and wrongly handled.
Apple dox
I know it's not the answer you wanted because this wasn't your doing but I personally would not proceed with a hack. I would restructure the code and trim the controller down to one scroll view.
My storyboard looks like this
and my code is the following
UIViewController
class DownLoadSoundsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// MARK: View Controller Properties
let viewName = "DownLoadSoundsViewController"
#IBOutlet weak var visualEffectView: UIVisualEffectView!
#IBOutlet weak var dismissButton: UIButton!
#IBOutlet weak var downloadTableView: UITableView!
// MARK: Properties
var soundPacks = [SoundPack?]() // structure for downloadable sounds
override func viewDidLoad() {
super.viewDidLoad()
downloadTableView.dataSource = self
downloadTableView.delegate = self
downloadTableView.register(DownLoadTableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfSoundPacks
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let method = "tableView.cellForRowAt"
//if (indexPath as NSIndexPath).section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "downloadTableViewCell", for: indexPath) as! DownLoadTableViewCell
cell.backgroundColor = UIColor.green
if soundPacks[(indexPath as NSIndexPath).row]?.price == 0 {
cell.soundPackPriceUILabel.text = "FREE"
} else {
cell.soundPackPriceUILabel.text = String(format: "%.2", (soundPacks[(indexPath as NSIndexPath).row]?.price)!)
}
//cell.textLabel?.text = soundPacks[(indexPath as NSIndexPath).row]?.soundPackTitle
cell.soundPackTitleUILabel.text = soundPacks[(indexPath as NSIndexPath).row]?.soundPackTitle
cell.soundPackAuthorUILabel.text = soundPacks[(indexPath as NSIndexPath).row]?.author
cell.soundPackShortDescription.text = soundPacks[(indexPath as NSIndexPath).row]?.shortDescription
cell.soundPackImage.image = UIImage(named: "Placeholder Icon")
DDLogDebug("\(viewName).\(method): table section \((indexPath as NSIndexPath).section) row \((indexPath as NSIndexPath).row))")
return cell
//}
}
UItableViewCell
class DownLoadTableViewCell: UITableViewCell {
#IBOutlet weak var soundPackImage: UIImageView!
#IBOutlet weak var soundPackTitleUILabel: UILabel!
#IBOutlet weak var soundPackAuthorUILabel: UILabel!
#IBOutlet weak var soundPackShortDescription: UILabel!
#IBOutlet weak var soundPackPriceUILabel: UILabel!
let gradientLayer = CAGradientLayer()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
But I get the following;
I am sure I am doing something small incorrectly, but as of yet can't figure it out. Looked through many examples included my own code where I have gotten this working before.
Not a single one of my settings for the tableview are getting invoked except the number of cells. But everything in;
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{...}
is not working.
Help is appreciated.
I think you need to reload the tableView after getting data from Firebase
self.saveMixesTableView.reloadData()
I have this custom UITableViewCell:
class CircleOfTrustTableViewCell: UITableViewCell {
#IBOutlet var permissionTitle: UILabel!
#IBOutlet weak var permissionImage: UIImageView!
#IBOutlet weak var permissionSwitch: UISwitch!
#IBOutlet weak var spacingView: UIView!
// Update toggle based on user visibility and category type
func update(showUser: Bool, index: Int){
self.tag = index
let type = ProfileContentTypeActual.all[index]
self.permissionImage.image = UIImage(named: categoryImages[type]!)
self.permissionTitle.text = categoryNames[type]
if showUser {
self.permissionSwitch.tag = index
self.permissionSwitch.isEnabled = true
if let toggles = GlintUser.getThisUser().trust_toggle {
self.permissionSwitch.setOn(toggles.getState(type), animated: true)
}
} else {
self.permissionSwitch.setOn(false, animated: true)
self.permissionSwitch.isEnabled = false
}
}
}
I implement the cell this way:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
settingsTableView.register(UINib(nibName: "COTTableViewCell", bundle: nil), forCellReuseIdentifier: "cotCell")
let cotCell = (settingsTableView.dequeueReusableCell(withIdentifier: "cotCell") as! CircleOfTrustTableViewCell)
cotCell.update(showUser: showUser, index: indexPath.row)
return cotCell
}
My question is: Is it best practice to call the update function on the cell (to populate its data) or is it best to leave this in the cellForRowAtIndexPath method?