so here's the problem that I'm facing.
Take a look
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExpenseTableViewCell_Title") as! ExpenseTableViewCell_Title
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExpenseTableViewCell_SaveCanel") as! ExpenseTableViewCell_SaveCanel
return cell
}
}
what i want to do is, use cell identifier string as cell type.(i.e. ExpenseTableViewCell_Title, ExpenseTableViewCell_SaveCanel).
I do have cell identifier array.
var TableCell:[ExpenseCellType] = [.ExpenseTableViewCell_Title, .ExpenseTableViewCell_SaveCanel]
Right now I only have two types of cell. But this number will go high.
And I don't want to use if/else condition or switch case.
Thank in advance.
Can make it shorter with extension:
extension UITableView {
func dequeueReusable<T>(type: T.Type, index: IndexPath) -> T {
return self.dequeueReusableCell(withIdentifier: String(describing: T.self), for: index) as! T
}
}
Use it like this, will return ExpenseTableViewCell_Title type cell:
let cell = tableView.dequeueReusable(type: ExpenseTableViewCell_Title.self, index: indexPath)
Just store your class in the array like [ExpenseTableViewCell_Title.self, ExpenseTableViewCell_SaveCanel.self] and pass it to this function
You can use function NSClassfromString but you will need namespace for getting class from String.
I have created example here to use it.
Example:
func getClassFromString(_ className: String) -> AnyClass! {
let namespace = Bundle.main.infoDictionary!["CFBundleExecutable"] as! String;
let cls: AnyClass = NSClassFromString("\(namespace).\(className)")!;
return cls;
}
class customcell: UITableViewCell {
}
let requiredclass = getClassFromString("customcell") as! UITableViewCell.Type
let cellInstance = requiredclass.init()
Related
I'd like to handle many different data types each of which has an associated UITableViewCell class and dedicated configure(for:) function.
Currently my view controller has something like this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func failure() {
fatalError("The row type was unknown, or the row item couldn't be cast as expected, or the specified cell couldn't be dequeued, or the dequeued cell couldn't be cast")
}
let section = sections[indexPath.section]
let item = section.rowItems[indexPath.row]
switch item.moduleType {
case .message:
guard let message = item as? CardModules.Message,
let messageCell = tableView.dequeueReusableCell(withIdentifier: CardCell_Message.identifier) as? CardCell_Message
else { failure(); break }
messageCell.configure(for: message)
return messageCell
case .availability:
guard let availability = item as? CardModules.Availability,
let availabilityCell = tableView.dequeueReusableCell(withIdentifier: CardCell_Availability.identifier) as? CardCell_Availability
else { failure(); break }
availabilityCell.configure(for: availability)
return availabilityCell
// etc, for many data types
I'd prefer a system where any model can have it's cell class instantiated with a single call in the view controller, and have written the following solution
protocol DataForCell {
associatedtype Cell: CellForData
func configuredCell(from tableView: UITableView) -> Cell
}
extension DataForCell {
func cell(from tableView: UITableView) -> Cell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell
return cell
}
}
protocol CellForData {
associatedtype Data: DataForCell
static var identifier: String { get }
func configure(for data: Data)
}
Which allows the cellForRowAt to just call configuredCell(from: tableView) on each item, as the array of items is an array of DataForCell
The question is, how can I improve further on this? It would be great if the configuredCell function could be moved into a protocol extension to avoid the need to keeping it as boilerplate in every instance of DataForCell
Finally, does this approach violate any important principles?
Update
Based on Sweepers suggestion, the improved protocols are this:
// MARK: - Data Protocol
protocol DataForCell {
associatedtype Cell: CellForData
}
extension DataForCell where Cell.Data == Self {
func configuredCell(from tableView: UITableView) -> Cell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell
cell.configure(for: self)
return cell
}
}
// MARK: - Cell Protocol
protocol CellForData {
associatedtype Data: DataForCell
static var identifier: String { get }
func configure(for data: Data)
}
Use different cell type all of them based on a common cell class.
class BaseCellType : UITableviewCell {
func configure(…) {
print(“Not implemented”)
}
}
class MessageCell : BaseCellTyoe {
override func configure(…) {
… configure the message cell
}
}
Then create a dictionary of cell identifiers in the TableViewDataSource :
let cellId : [ModuleType:String] = [.message : CardModule.message, …]
Then in cellForRow , dequeue cell with correct identifier for type BaseCellType and call configure.
In storyboard set the final cell type for each cell.
Note : you may also do this with a protocole.
Is there any way for a tableview to read as a list for accessibility while having the whole focus on the tableview?
For example: I have a list like
Art
Ball
Car
Dog
So I would want the accessibility reader to read as "Item 1 of 4 Art, Item 2 of 4 Ball, .... etc"
Yes, you can but you would have to implement that manually.
You can create some kind of model for your cell which you use to configure it.
You would need to pass the total row count of your table view to that configuration for each cell.
struct CellConfig {
let title: String
private let count: Int
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
You could than actually extend the functionality to let the CellConfig return the correct accessibility label by passing the current IndexPath like so:
struct CellConfig {
...
func axLabel(for indexPath: IndexPath) -> String {
let currentElement = indexPath.row + 1
return "Item \(currentElement) of \(count). \(title)."
}
}
So when returning your cell from your delegate method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard indexPath.row < items.count else { return UITableViewCell() }
let item = items[indexPath.row] // The array here holds all the configs of every cell.
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as? UITabelViewCell
cell?.titleLabel.text = item.title
cell?.accessibilityLabel = item.axLabel(for: indexPath)
return cell ?? UITableViewCell()
}
I have three different types of custom UITableCells. I have an if statement that sets them up:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if somePosts[indexPath.row].typeOfPost == .linkPost {
let cell: LinkTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "linkTableViewCell") as! LinkTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .picturePost {
let cell: PictureTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "pictureTableViewCell") as! PictureTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .textPost {
let cell: TextTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "textTableViewCell") as! TextTableViewCell
} else {
print("Type of post is not link, picture, or text")
}
}
Each of the custom cells has similar labels such as title and time. I would like to set these labels using the same line of code, such as:
cell.titleLabel.text = "Some title here"
However, in this example, I get an error saying I am using an unresolved identifier "cell," obviously because my variables are being declared non-globally. Is there a way around this since swift is strongly typed? Thanks!
Make a protocol that your TableViewCell classes extend, and store cell as a variable of that type.
protocol MyTableViewCell {
var titleLabel: UILabel { get }
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier: String
switch somePosts[indexPath.row].typeOfPost {
case .linkPost: identifier = "linkTableViewCell"
case .picturePost: identifier = "pictureTableViewCell"
case .textPost: identifier = "textTableViewCell"
default: fatalError("Type of post is not link, picture, or text")
}
guard let cell = self.tableView.dequeueReusableCell(withIdentifier: identifier) as? MyTableViewCell else {
fatalError("Cell isn't castable to MyTableViewCell")
}
cell.titleLabel.text = "Some title here"
// ...
}
You have three basic solutions.
Repeat cell.text = ... inside each block. But this isn't what you really want as stated in your question.
Have your three custom cell classes all extend a common base class. Have this base class define any common properties.
Define a protocol with the common properties and have each of your custom cell classes conform to the protocol.
For options 2 and 3 you would declare a variable of the base/protocol type before the first if statement. Then after the whole if/else block, you can assign any of the common properties.
If you need to update any cell type specific properties, you can do that inside the appropriate block as well.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell?
if somePosts[indexPath.row].typeOfPost == .linkPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "linkTableViewCell") as! LinkTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .picturePost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "pictureTableViewCell") as! PictureTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .textPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "textTableViewCell") as! TextTableViewCell
} else {
print("Type of post is not link, picture, or text")
}
if let cell = cell {
cell.commonProperty = ...
return cell
} else {
return nil // this shouldn't happen but if it does, you have a bug to fix
}
}
If the subclasses each have their own titleLabel property, you will need to make them all conform to a protocol. Let's call it ConfigurableCell.
protocol ConfigurableCell {
var titleLabel: UILabel { get set }
}
Then, you can initialize your cells all the same way, but declare them as a ConfigurableCell:
var cell: ConfigurableCell? = nil // not set yet
if somePosts[indexPath.row].typeOfPost == .linkPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "linkTableViewCell") as! LinkTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .picturePost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "pictureTableViewCell") as! PictureTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .textPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "textTableViewCell") as! TextTableViewCell
}
guard let cell = cell else {
// how to handle this error case is up to you
print("Type of post is not link, picture, or text")
return UITableViewCell()
}
// now, cell is a ConfigurableCell with a titleLabel property, regardless of class
cell.titleLabel.text = "Some title"
Of course, UITableViewCell does have a built-in textLabel property, which you could try to utilize in your cell classes, and then a protocol wouldn't be necessary, because the property is in UITableViewCell.
I have encountered an error in swift when attempting to create a tableview made up of custom cells dependent upon a set of conditions.
Here is my code:
var tableData: [String] = []
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let phonenocell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier("phonecell", forIndexPath: indexPath) as! MyCustomCell
let pincell:SocialCell = self.tableView.dequeueReusableCellWithIdentifier("socialcell", forIndexPath: indexPath) as! SocialCell
let fbcell:FacebookCell = self.tableView.dequeueReusableCellWithIdentifier("facebookcell", forIndexPath: indexPath) as! FacebookCell
let snapcell:SnapchatCell = self.tableView.dequeueReusableCellWithIdentifier("snapchatcell", forIndexPath: indexPath) as! SnapchatCell
let twitcell:TwitterCell = self.tableView.dequeueReusableCellWithIdentifier("twittercell", forIndexPath: indexPath) as! TwitterCell
let instacell:InstagramCell = self.tableView.dequeueReusableCellWithIdentifier("instagramcell", forIndexPath: indexPath) as! InstagramCell
if tableData.contains("Number") {
return phonenocell
}
if tableData.contains("Social") {
return pincell
}
if tableData.contains("Facebook") {
return fbcell
}
if tableData.contains("Snapchat") {
return snapcell
}
if tableData.contains("Twitter") {
return twitcell
}
if tableData.contains("Instagram") {
return instacell
}
}
When attempting to build and run I get a build failed with the following fault:
"Missing Return in a function expected to return 'UITableViewCell'
I have been over and over my code but I honestly cannot see where I am going wrong...
Any help would be greatly appreciated!
You need to return cell for sure.
You already do in conditions, but in case none of your condition statements would success, your return call wouldn't be fired.
Appending, for example:
return phonenocell
to the end of the function, should be quick fix for your code. It ensures, that the function will return a cell (that is mandatory).
My data source is the array tableData. This is constructed on the previous view as: #IBAction func switch1Toggled(sender: UISwitch) { if mySwitch1.on { fbTextBox.text = "Selected" dataArray.append("Facebook")
And this may be the main issue:
Assuming, that you choose 'facebook' and that you reload your tableView, every row will pass the first condition as it IS contained.
You should put this in your method:
//assuming your data source contains multiple members, and your numberOfRowsInSections... method return tableData.count, you need to get each item for each row:
let currentTag = tableData[indexPath.row]
if (currentTag == "something") { //e.g. Facebook
let somethingcell:MySomethingCell = ...
self.tableView.dequeueReusableCellWithIdentifier("somethingcell", forIndexPath: indexPath) as! MySomethingCell
return somethingcell
} else if {
...
}
return emptycell //this line is just for the case, when no of your conditions will pass and you don't catch all the situations...
maybe your array elements doesn't match the condition, it's better to return default value instead of ur conditions failed
I need to get the first cell in my tableView to be a different size from the rest. The rest of my cells are all under the class CustomPFTableViewCell, but the first one is a different cell so its under the class FirstPFTableViewCell, both of which extend from the class PFTableViewCell. Right now, I just used an if depending on the indexPath.row for whether or not the cell was the first cell. When its not it will load data for the cell from Parse.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell {
if(indexPath.row >= 1){
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! CustomPFTableViewCell!
print("Loading Parse Database Files...")
// Extract values from the PFObject to display in the table cell
if let name = object?["Name"] as? String {
cell?.nameTextLabel?.text = name
print("Loading " + name)
}
if let author = object?["authorName"] as? String {
cell?.authorTextLabel?.text = author
}
if let likes = object?["Likes"] as? Int {
let stringVal = String(likes)
cell?.numLikes.text = stringVal
}
if let descrip = object?["Description"] as? String {
cell?.descriptionHolder = descrip
}
let initialThumbnail = UIImage(named: "Unloaded")
cell.customFlag.image = initialThumbnail
if let thumbnail = object?["imageCover"] as? PFFile {
cell.customFlag.file = thumbnail
cell.customFlag.loadInBackground()
}
return cell
}
print("Finished loading!")
let cell = tableView.dequeueReusableCellWithIdentifier("firstCell") as! PFTableViewCell
return cell
}
The end is empty because I'm not sure how to go about changing the one/first cell's size. (In the Interface Builder its set to 60). I guess the most important part in solving this is this:
let cell = tableView.dequeueReusableCellWithIdentifier("firstCell") as! PFTableViewCell
return cell
}
In order to play with the size of the cell you have to implement the UITableViewDelegate function
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == 0 {
return firstCellHeight
} else {
return customCellHeight
}