I am new to swift I have a tableView with a custom cell. When I hit the cell, it should expand the cell and show all the data. I tried but I am not able to get the proper result. I have to show a custom table view cell when I will show all the data
data. I have the data coming from json.
Custom xib table view cell:
class ExpandTableViewCell: UITableViewCell {
#IBOutlet weak var timeLable: UILabel!
var item: ExpandTableViewCell? {
didSet {
}
}
static var nib:UINib {
return UINib(nibName: identifier, bundle: nil)
}
static var identifier: String {
return String(describing: self)
}
}
Register into the viewDidLoad():
tableView?.register(ExpandTableViewCell.nib, forCellReuseIdentifier: ExpandTableViewCell.identifier)
The tableView methods:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 1 {
if storeDetailsDictionary != nil{
return (storeDetailsDictionary?.count)!
} else {
return 0
}
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.section == 1) {
//address cell
let cell = tableView.dequeueReusableCell(withIdentifier: ExpandTableViewCell.identifier, for: indexPath) as? ExpandTableViewCell
var timing:String = String.init(htmlEncodedString: storeDetailsDictionary!["timing"] as? String ?? "")
cell?.timeLable.text = timing//?.replacingOccurrences(of:", ", with: ",\n")
return cell!
}
}
How can I expand the table view cell?
check this tutorial it's helpful for creating expandable list:
https://medium.com/ios-os-x-development/ios-how-to-build-a-table-view-with-collapsible-sections-96badf3387d0
Related
This is my code:-
Model:-
class QuestionListModel: NSObject {
var optionsModelArray:[OptionsModel] = []
var question:String!
init(dictionary :JSONDictionary) {
guard let question = dictionary["question"] as? String
else {
return
}
if let options = dictionary["options"] as? [String]{
print(options)
print(options)
for values in options{
print(values)
let optionmodel = NH_OptionsModel(values: values)
self.optionsModelArray.append(optionmodel)
}
}
self.question = question
// print(self.dataListArray33)
}
}
optionModel:-
class OptionsModel: NSObject {
var values:String?
init(values:String) {
self.values = values
print( self.values)
}
}
in viewmodel:-
var questionsModelArray:Array<NH_QuestionListModel>? = []
init(withdatasource newDatasourceModel:NH_QuestionDataSourceModel) {
datasourceModel = newDatasourceModel
print(datasourceModel.dataListArray?.count)
self.questionsModelArray = datasourceModel.dataListArray
print(self.questionsModelArray)
print(datasourceModel.dataListArray)
}
func numberOfSections() -> Int{
return (self.questionsModelArray?.count)!
}
func titleForHeaderInSection(atindexPath indexPath: IndexPath) -> QuestionListModel {
return self.questionsModelArray![indexPath.row]
}
func numberOfRowsInSection(indexPath:IndexPath) -> Int {
if let questionModel = self.questionsModelArray?[indexPath.section]{
return questionModel.optionsModelArray.count
}
else{
return 0
}
}
func datafordisplay(atindex indexPath: IndexPath) -> OptionsModel{
let questionModel = self.questionsModelArray?[indexPath.section]
return questionModel!.optionsModelArray[indexPath.row]
}
And in ViewController:-
func numberOfSections(in tableView: UITableView) -> Int {
return questionViewModel.numberOfSections()
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: IndexPath) -> UIView? {
// let headercell = Bundle.main.loadNibNamed("HeaderCell", owner: self, options: nil)?.first as! NH_questionheader
let identifier = "HeaderCell"
var headercell: NH_questionheader! = tableView.dequeueReusableCell(withIdentifier: identifier) as? NH_questionheader
if headercell == nil {
tableView.register(UINib(nibName: "NH_questionheader", bundle: nil), forCellReuseIdentifier: identifier)
headercell = tableView.dequeueReusableCell(withIdentifier: identifier) as? NH_questionheader
}
headercell.setReviewData(reviews:questionViewModel.titleForHeaderInSection(atindexPath:section))
return headercell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 150
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: IndexPath) -> Int {
return questionViewModel.numberOfRowsInSection(indexPath: section)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "Cell"
var cell: CellTableViewCell! = tableView.dequeueReusableCell(withIdentifier: identifier) as? CellTableViewCell
if cell == nil {
tableView.register(UINib(nibName: "CellTableViewCell", bundle: nil), forCellReuseIdentifier: identifier)
cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? CellTableViewCell
}
cell.contentView.backgroundColor = UIColor.clear
cell.setOptions(Options1: questionViewModel.datafordisplay(atindex: indexPath))
print("Section \(indexPath.section), Row : \(indexPath.row)")
return cell
}
my json file:-
{
"data":[
{
"question": "Gender",
"options": ["Male","Female"]
},
{
"question": "How old are you",
"options": ["Under 18","Age 18 to 24","Age 25 to 40","Age 41 to 60","Above 60"]
}, {
"question": "I am filling the Questionnaire for?",
"options": ["Myself","Mychild","Partner","Others"]
}
]
}
This is my data .So i need to display the questions in header and options in the cell for index .But showing as error as UITableview has need to conform the protocol UITableviewDataSource.
Also showing error as Index out of range.
How to do.....
I think you are not assign a datasource to your view controller. So please assign it in your ViewDidLoad of your view controller
override func viewDidLoad() {
super.viewDidLoad()
self.yourtableview.delegate = self
self.yourtableview.dataSource = self
// Do any additional setup after loading the view.
}
This error usually occurs when you fail to implement the required methods of a protocol. In this case the methods would be :
cellForRowAt
numberOfRowsInSection
Since you already have them implemented in your view controller chances are that you might have failed to set the datasource for the table view.
Refer to this
https://developer.apple.com/documentation/uikit/uitableviewdatasource
your view controller cannot find the data source and delegate for the table view. make sure you have assigned the data source and delegate
self.yourtableview.delegate = self
self.yourtableview.dataSource = self
and also make sure that your controller also inherit the UITableViewDelegate and UITableViewDataSource like this
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource
To achieve what you want, you should set your VC as the delegate and datasource of your table.
Option 1, do it dynamically:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
}
Option 2, from your storyboard (example below):
After this, you should use the following datasource functions of UITableView:
// return number of questions
func numberOfSections(in tableView: UITableView) -> Int
// return number of options per question (indicated by section)
func tableView(UITableView, numberOfRowsInSection: Int) -> Int
You haven't correctly declared the numberOfRowsInSection function; section is an Int, not an IndexPath. As a result you have not implemented the mandatory functions of UITableViewDataSource.
You want:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return questionViewModel.numberOfRowsIn(section: section)
}
With an appropriate change in your view model:
func numberOfRowsIn(section:Int) -> Int {
return self.questionsModelArray?[section].optionsModelArray.count ?? 0
}
I would also suggest that you review your use of implicitly unwrapped optionals and force unwrapping; this is just asking for crashes.
For example, there is no reason for the question property of QuestionListModel to be String!; just declare it as String and make your initialiser failable. Better yet, use Codable to create your model from JSON and get rid of all of that code.
You can eliminate the force unwrapping in numberOfSections too:
func numberOfSections() -> Int {
return self.questionsModelArray?.count ?? 0
}
I would also suggest you make QuestionListModel a struct rather than an NSObject subclass.
If I were you I would re-factor to remove the view model, it is adding unnecessary complexity in this case, and use Codable for your JSON deserialisation:
struct Questions: Codable {
enum CodingKeys: String, CodingKey {
case questions = "data"
}
var questions: [Question]
}
struct Question: Codable {
var question: String
var options: [String]
}
Your view controller then becomes much simpler:
class ViewController: UIViewController, UITableViewDatasource {
var questionData: Questions?
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "NH_questionheader", bundle: nil), forCellReuseIdentifier: "HeaderCell")
tableView.register(UINib(nibName: "CellTableViewCell", bundle: nil), forCellReuseIdentifier: "Cell")
// You don't show how you load your JSON, but assuming you have it in an instance of `Data` called `jsonData`:
do {
self.questionData = try JSONDecoder().decode(Questions.self, from: jsonData)
} catch {
print("Error decoding JSON: \(error.localizedDescription)")
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: IndexPath) -> UIView? {
let identifier = "HeaderCell"
guard let questionData = self.questionData,
let headercell = tableView.dequeueReusableCell(withIdentifier: identifier) as? NH_questionheader else {
return nil
}
headercell.label.text = questionData.questions[section].question
return headercell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 150
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.questionData?.questions[section].options.count ?? 0
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.questionData?.questions.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "Cell"
// Note, I have used force unwrapping and a forced downcast here as if either of these lines fail you have a serious problem and crashing is the simplest way of finding it during development
let option = self.questionData!.questions[indexPath.section].options[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath ) as! CellTableViewCell
cell.contentView.backgroundColor = .clear
cell.label.text = option
return cell
}
}
Once you have this basic approach working you can try and add a view model if you like.
I have a tableview with one textfield in each cell. I added a target like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customLevelCell") as! LevelTableViewCell
cell.cellTextField.addTarget(self, action: #selector(ViewController.TextfieldEditAction), for: .editingDidEnd)
return cell
}
But found out that I'm not able to use the indexpath.row / sender.tag to get the specific textfield text
#objc func TextfieldEditAction(sender: UIButton) {
}
So my question is how can I get the text after the user has edited one of the textfields.
Also how can i get the indexpath.row or sender.tag which will be used to collect the text they added to that specific textfield.
The easiest way to handle this is probably to use a delegate protocol…
In your cell
protocol LevelTableViewCellDelegate: class {
func levelTableViewCell(_ levelTableViewCell: LevelTableViewCell, didEndEditingWithText: String?)
}
class LevelTableViewCell: UITableViewCell {
#IBOutlet private weak var cellTextField: UITextField!
var delegate: LevelTableViewCellDelegate?
override func awakeFromNib() {
cellTextField.addTarget(self, action: #selector(didEndEditing(_:)), for: .editingDidEnd)
}
#objc func didEndEditing(_ sender: UITextField) {
delegate?.levelTableViewCell(self, didEndEditingWithText: sender.text)
}
}
In your view controller
class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LevelTableViewCell") as! LevelTableViewCell
cell.delegate = self
return cell
}
}
extension TableViewController: LevelTableViewCellDelegate {
func levelTableViewCell(_ levelTableViewCell: LevelTableViewCell, didEndEditingWithText: String?) {
let indexPath = tableView.indexPath(for: levelTableViewCell)
// Now you have the cell, indexPath AND the string
}
Also, note that the view outlet is be private. You'll find that you write cleaner code if you follow this rule
Following is the extension of UIView that can be used to get the cell or indexPath of the cell enclosing textField
extension UIView {
var tableViewCell : UITableViewCell? {
var subviewClass = self
while !(subviewClass is UITableViewCell){
guard let view = subviewClass.superview else { return nil }
subviewClass = view
}
return subviewClass as? UITableViewCell
}
func tableViewIndexPath(_ tableView: UITableView) -> IndexPath? {
if let cell = self.tableViewCell {
return tableView.indexPath(for: cell)
}
return nil
}
}
Example :-
#objc func TextfieldEditAction(sender: UITextField) {
//replace tableView with the name of your tableView
guard let indexPath = sender.tableViewIndexPath(tableView) else {return}
}
I am using nested tableview. The main tableview lists the file categories. Child tableview listing the files. I open the files with safari. The child tableview is listed incorrectly when I go back to the page after opening the file. How can i solve this problem? Android sdk have "onActivityResult" method. Does iOS have a similar function? Thanks.
ViewController
import UIKit
class ProductDetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
var bundleProductModel:ProductModel? = ProductModel.init()
var lastFileCatIndex:Int = 0
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If tableview is file category table.
if (tableView.tag == 100){
return bundleProductModel!.fileCategoryModels.count
} else /* Table view is file tableview. */ {
//self.lastFileIndex = self.lastFileIndex + 1
return (bundleProductModel?.fileCategoryModels[self.lastFileCatIndex].files.count)!
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (tableView.tag == 100){
// Define cell for file category.
let cell = tableView.dequeueReusableCell(withIdentifier: "FileCategoryTableViewCell") as! FileCategoryTableViewCell
// Set file category cell height.
cell.frame.size.height = CGFloat(((bundleProductModel?.fileCategoryModels[indexPath.row].files.count)! * 44) + 42)
// cell row height
tableView.rowHeight = CGFloat(((bundleProductModel?.fileCategoryModels[indexPath.row].files.count)! * 44) + 42)
// Control bound
if (self.lastFileCatIndex <= indexPath.row){
// Index.
self.lastFileCatIndex = indexPath.row
// File category name.
cell.lblFileCatNme.text = " \(bundleProductModel?.fileCategoryModels[indexPath.row].file_category_name ?? "Unknow") "
}
return cell
} else {
// Define cell for files.
let cell = tableView.dequeueReusableCell(withIdentifier: "FileTableViewCell") as! FileTableViewCell
if ((bundleProductModel?.fileCategoryModels[self.lastFileCatIndex].files.count)! > indexPath.row){
// Set file model to file cell.
cell.setFile(fileItem: (self.bundleProductModel?.fileCategoryModels[self.lastFileCatIndex].files[indexPath.row])!)
// file cell delegate
cell.delegate = self
} else {
cell.lblFileName.text = "unknow"
}
return cell
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ProductDetailViewController:FileCellDelegate{
func didClickDownload(downloadLink: String, button: UIButton) {
if let url = URL(string: downloadLink) {
UIApplication.shared.open(url)
}
}
}
A very easy workaround on iOS would be to override viewWillAppear and call reloadData() like so:
override func viewWillAppear() {
super.viewWillAppear()
tableView.reloadData()
}
This will update your table everytime your view reappears.
SOLVED
Problem is lastFileCategoryIndex variable. Ex: final value is four. When I come back to the page; listing relative to fourth index. I define child tableview in main tableview cell and solved.
FileCategoryTableViewCell
class FileCategoryTableViewCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource {
// General Objects
var fileCategoryModel:FileCategoryModel = FileCategoryModel.init()
// Cell Ui Objects
#IBOutlet weak var lblFileCatNme: UILabel!
#IBOutlet weak var fileTableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fileCategoryModel.files.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = fileTableView.dequeueReusableCell(withIdentifier: "FileTableViewCell") as! FileTableViewCell
cell.lblFileName.text = "Ex File..."
return cell
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
// Set category model.
func setFileCategory(fileCategoryModel:FileCategoryModel){
self.fileCategoryModel = fileCategoryModel
self.fileTableView.dataSource = self
self.fileTableView.delegate = self
}
}
I have some data which is pulled from a DB and stored in an array within my UITableViewController. However, when I try to put the data inside each of my cells, I get an error that my index is out of the bounds of the array.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellItems.count
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item: ItemPreBasketModel = cellItems[indexPath.row] as! ItemPreBasketModel
if indexPath.row == 0 {
//Desc cell
let cellIdentifier: String = "descCell"
let descCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
return descCell
} else if indexPath.row == 1 {
//Price cell
let cellIdentifier: String = "priceCell"
let priceCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
return priceCell
} else if indexPath.row == 2 {
//Quantity cell
let cellIdentifier: String = "quantityCell"
let quantityCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
return quantityCell
} else {
//Submit cell
let cellIdentifier: String = "submitCell"
let submitCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
return submitCell
}
}
Here is the cellItems variable and also where it is populated:
var cellItems: NSArray = NSArray() {
didSet {
tableView.reloadData()
}
}
func itemsDownloaded(items: NSArray) {
cellItems = items
tableView.reloadData()
}
Here is the static table view in the storyboard:
What I want to do, which I've took out of the code for as it still fails before reaching this part, is to use the 'item' variable I declare inside cellForRowAt and populate each of my cell outlets with a part of the object,
i.e: descCell.descLabel.text = item.name
The error I get is:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
on the line
let descCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
You should not use a table view controller with static cells and cellForRowAt at the same time. You should use dynamic prototypes to populate an array dynamically.
To change the content type of your table view, select it in Interface Bulder, open the Attributes inspector (the fourth icon from the left in the right sidebar) and select Dyanmic Prototypes for Content under the Table View section.
So, I have method loadData() which download datas from parse.com
And I should present all images show in table view.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ReusableCell", forIndexPath: indexPath) as! LeaguesTableViewCell
loadData { (success) in
if success {
cell.leagueImage.image = UIImage(data: self.leaguesImage[indexPath.row])
cell.leagueNameLabel.text = self.leagues[indexPath.row]["name"] as? String
} else {
cell.leagueNameLabel.text = "Wait"
}
}
return cell
}
Its didn't work. I call my function in viewDidLoad() but its not correct too, table view is empty.
Cuz my array is empty
My
The basic procedure for loading data into a UITableView is:
Load the data
Reload the table view
Return the number of sections in numberOfSectionsInTableView: method: In your case there is only 1 section.
Return the number of rows in tableView:numberOfRowsInSection:: In your case return the number of leagues if the data is loaded. If the data is not loaded then return 1 so that the table view has at least one row to display the "Wait" message.
Create and populate the cells from the data: Use leagues and leaguesImage.
Example:
private var loaded = false
override func viewDidLoad() {
super.viewDidLoad()
loaded = false
loadData() { success in
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.loaded = success
self.tableView.reloadData()
}
}
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection: Int) -> Int {
if loaded {
return leagues.count
}
else {
return 1
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ReusableCell", forIndexPath: indexPath) as! LeaguesTableViewCell
if loaded {
cell.leagueImage.image = UIImage(data: self.leaguesImage[indexPath.row])
cell.leagueNameLabel.text = self.leagues[indexPath.row]["name"] as? String
}
else {
cell.leagueNameLabel.text = "Wait"
}
return cell
}
Try to set delegate and datasource first. If you have separate datasource other than view controller, retain it otherwise you will not get any callback.