I'm trying to refine some working but ugly code.
My app has five TableViews, each one displaying a different type of data (with different cell layouts). Because the datatypes are similar-ish and require many similar methods (for downloading, encoding, etc), I have set up a TableViewController:UITableViewController class to serve as a superclass for the five TableViewController subclasses. Within this superclass, I have the standard "cellForRowAt" method, but it's bloated and repetitive. I want to simplify it.
My problem (I think) is the multiple "let cell = " statements, which all cast as a different type of TableViewCell depending on the datatypes. For example, my DataType.SCHEDULES datatype needs to get a SchedulesTableViewCell with reuseID of "SchedulesCell". I can't make them all the same TableViewCell class, because they each have their own IBOutlet views.
Making things uglier, each tableView has two cell prototypes, and I need to be able to generate an ARTICLE cell and a DETAIL cell for each datatype.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// get the article and row type
let article = getArticleFor(indexPath: indexPath)
let cellType = getCellTypeFor(indexPath: indexPath)
// create either an ARTICLE row or a DETAIL row.
// (simplified for SO posting. Each "case" is actually
// 5-6 lines of nearly identical code)
switch cellType {
// for the ARTICLE cell prototype
case CellType.ARTICLE:
// get the right table cell matching the datatype
switch self.datatype {
case DataType.SCHEDULES:
let cell = tableView.dequeueReusableCell(withIdentifier: "SchedulesCell") as! SchedulesTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.LUNCH:
let cell = tableView.dequeueReusableCell(withIdentifier: "LunchCell") as! LunchTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.EVENTS:
let cell = tableView.dequeueReusableCell(withIdentifier: "EventsCell") as! EventsTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.DAILY_ANN:
let cell = tableView.dequeueReusableCell(withIdentifier: "DailyannCell") as! DailyannTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.NEWS:
let cell = tableView.dequeueReusableCell(withIdentifier: "NewsCell") as! NewsTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
}
// or for the DETAIL cell prototype
case CellType.DETAIL:
// get the right table cell matching the datatype
switch self.datatype {
case DataType.SCHEDULES:
let cell = tableView.dequeueReusableCell(withIdentifier: "SchedulesDetailsCell") as! ScheduleDetailTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.LUNCH:
let cell = tableView.dequeueReusableCell(withIdentifier: "LunchDetailsCell") as! LunchDetailsTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.EVENTS:
let cell = tableView.dequeueReusableCell(withIdentifier: "EventsDetailsCell") as! EventsDetailTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.DAILY_ANN:
let cell = tableView.dequeueReusableCell(withIdentifier: "DailyannDetailCell") as! DailyannDetailsTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
case DataType.NEWS:
let cell = tableView.dequeueReusableCell(withIdentifier: "NewsDetailCell") as! NewsDetailTableViewCell
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
return cell
}
}
}
I originally had each "let cell =" case within the subclasses' own "cellForRowAt" methods, but I was repeating very similar code in every subclass, which seemed silly. On the other hand, the code above moved the repetition into a single class, but didn't remove the repetition, so it's still silly, but in a different place.
I feel like if I could make a dictionary of classes, something like this...
let tableCellClasses = [DataType.SCHEDULES : ScheduleTableViewCell,
DataType.LUNCH : LunchTableViewCell
etc.
...then I could make my "let cell = " statements more generic, like...
let cell = tableView.dequeueReusableCell(withIdentifier: identifier[dataType]) as! tableCellClasses[dataType]
but can't seem to find a way to make it work.
As I said, it works but it's ugly. I work in a high school, so I'd like for students viewing the repo to see clean, well-structured code -- so I'm shooting for better than just "it works."
Any suggestions?
You could use Swift's meta types and what not, but looking at your code, all your cell subclasses share the same methods:
cell.fillCellWith(article: article)
cell.otherMethod2()
cell.otherMethod3()
Why not:
Have a base class from which all custom cell classes inherit, that implements the above interface (the three methods you use after dequeuing a cell, with the possibility of them being overriden on each concrete subclass), so dequeue once and force-cast into the base type (I believe the right implementation of the methods will be executed, for each subclass. The cast is only to make the compiler happy: UITableViewCell does not have those methods).
Have a switch clause on the data type that gives you the specific cell identifier
Have each prototype cell set to the specific class on the storyobard, and assign the specific identifier too.
Does it make sense?
Now, form looking at your code, it doesn't look like you really need different subclasses. It's perfectly okay to have several different protoypes of the same UITableViewCell subclass, each with a different subview layout and a different reuse identifier, as long as they all can work with the same number and type of subviews and other custom properties/methods.
There is no way to do what you assume. You have to casting the classes one by one for your need. Or you can use base class which implemented all methods you need and calling them by the datatype.
Related
I wonder if anyone can offer any guidance? I am writing an iPhone app, using Xcode 13.2.1. I am displaying a tableview within a scene that uses XIBs. It works fine. Above the table I have a header that is being displayed, it too works fine.
However, what I'd like to do is display the header, then display a cell that doesn't use the XIB (and that is a height of 50), and then displays every other cell after that first cell using a XIB (height is 195 - just an FYI). Thus, to do/implement this what I am trying to do is implement some kind of 'if statement' such that if indexPath.row is 0 then set the cell type to <call it cell type 1>, and if the indexPath.row is not 0 then set the cell type to <call it cell type 2>. I don't believe that I can use an IF statement because later in the code block it won't recognise the value of cell because it would have been set in an IF statement. Hence, I think I need to use a turnery operator, however I am struggling to construct the turnery operator.
The current code that sets up the cell for XIB in the
// MARK: TableView CELL Information
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Current code that sets up the cell for a XIB template
guard let cell: CustomTableViewCellTypeA = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomTableViewCellTypeA else {
os_log("Dequeued cell isn't an instance of CustomTableViewCellTypeA", log: .default, type: .debug)
fatalError()
}
I KNOW THE FOLLOWING CODE DOESN'T WORK - however I am showing it this way to try and explain what I am trying to achieve:
// Intent is to use an IF or turnery operator to set the correct cell type
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
} else {
// Current code that sets up the cell for a XIB template
guard let cell: CustomTableViewCellTypeA = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomTableViewCellTypeA else {
os_log("Dequeued cell isn't an instance of CustomTableViewCellTypeA", log: .default, type: .debug)
fatalError()
}
}
The follow on code (if I can somehow get the above to work) would mean that I would display some information in the first cell which would be <call it cell type 1> and then display other information in <call it cell type 2>.
Anyone done this before or would have any guidance on how to create such a turnery operator? I have tried many things but can't seem to manage to find the solution.
Cheers James.
Actually this was much easier to solve than trying to add complexity of turnery operators to determine which cell to dequeue. It was simply a case of using an IF and adding in the code I wanted to execute along with ensuring I put a return cell statement in it, meaning that if the IF-statement wasn't executed then the code executes the other dequeue statement... Thus, the code looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row < 1 {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Select/tap on an event record below to edit the details of the journey."
cell.textLabel?.textAlignment = .center
cell.textLabel?.numberOfLines = 0
cell.backgroundColor = .orange
return cell
}
guard let cell: CustomTableViewCellTypeA = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomTableViewCellTypeA else {
os_log("Dequeued cell isn't an instance of CustomTableViewCellTypeA", log: .default, type: .debug)
fatalError()
}
This gave me the result I needed. Also, the feedback from Shawn Frank above helped me realise I hadn't registered the first cell type (only the second one), thus when I registered both the first and second cell types within the class it all worked beautifully. Thank you to all who looked at the question and the fine folks above who gave guidance. Cheers James.
I'm looking at DiffableDataSource available in iOS13 (or backported here: https://github.com/ra1028/DiffableDataSources) and cannot figure out how one would support multiple cell types in your collection or tableview.
Apple's sample code1 has:
var dataSource: UICollectionViewDiffableDataSource<Section, OutlineItem>! = nil
which seems to force a data source to be a single cell type. If I create a separate data source for another cell type - then there is no guarantee that both data sources don't have apply called on them at the same time - which would lead to the dreaded NSInternalInconsistencyException - which is familiar to anyone who has attempted to animate cell insertion/deletion manually with performBatchUpdates.
Am I missing something obvious?
I wrapped my different data in an enum with associated values. In my case, my data source was of type UICollectionViewDiffableDataSource<Section, Item>, where Item was
enum Item: Hashable {
case firstSection(DataModel1)
case secondSection(DataModel2)
}
then in your closure passed into the data source's initialization, you get an Item, and you can test and unwrap the data, as needed.
(I'd add that you should ensure that your backing associated values are Hashable, or else you'll need to implement that. That is what the diff'ing algorithm uses to identify each cell, and resolve movements, etc)
You definitely need to have a single data source.
The key is to use a more generic type. Swift's AnyHashable works well here. And you just need to cast the instance of AnyHashable to a more specific class.
lazy var dataSource = CollectionViewDiffableDataSource<Section, AnyHashable> (collectionView: collectionView) { collectionView, indexPath, item in
if let article = item as? Article, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section.articles.cellIdentifier, for: indexPath) as? ArticleCell {
cell.article = article
return cell
}
if let image = item as? ArticleImage, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section.trends.cellIdentifier, for: indexPath) as? ImageCell {
cell.image = image
return cell
}
fatalError()
}
And the Section enum looks like this:
enum Section: Int, CaseIterable {
case articles
case articleImages
var cellIdentifier: String {
switch self {
case .articles:
return "articleCell"
case .articleImages:
return "imagesCell"
}
}
}
One way of achieving this could be taking advantage your Section enum to identify the section with indexPath.section. It will be something like this:
lazy var dataSource = UICollectionViewDiffableDataSource<Section, Item> (collectionView: collectionView) { collectionView, indexPath, item in
let section = Section(rawValue: indexPath.section)
switch section {
case .firstSection:
let cell = ... Your dequeue code here for first section ...
return cell
case .secondSection:
let cell = ... Your dequeue code here for second section ...
return cell
default:
fatalError() // Here is handling the unmapped case that should not happen
}
}
I'm trying to set up a tableview that has 2 cells. Both cells have their own classes and they have their own methods to configure them. In code, when i get to the cellforrowatindexpath method, i get stuck. I can only dequeue one of the cells and call it's methods, which means the other cell won't configured. I want to configure both. Here's what i'm (currently) trying in the cellforrow method:
let cells = [tableView.viewWithTag(1), tableView.viewWithTag(2)]
for view in cells {
var reuseIdentifier = "cellID"
switch view?.tag {
case 1: // error occurs here
reuseIdentifier = "storyCell"
let storyCell1 = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! StoryCell1
storyCell1.theCategory.text = newsItem.storyCategory
storyCell1.theTitile.text = newsItem.titleText
storyCell1.firstParagraph.text = newsItem.paragraph1
case 2: // error occurs here too
reuseIdentifier = "storyCell2"
let storyCell2 = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! StoryCell2
storyCell2.configureCell(newsItem)
default:
}
in the storyboard, i have given both those cells tags of 1 and 2 respectively, hence the array at the very beginning. I have 2 problems with this approach.
I can't use it because the switch statement is throwing me an error: Expression pattern of type 'Int' cannot match values of type 'Int?'
even if it were to not have the error above, i can still only return one cell at the end of the method.
any help on my approach or a different, better way to handle this would be appreciated. Thanks!
EDIT:
Since I'm sure i've added the tag property i force unwrapped the view!.tag property and the error goes away. So, the 2nd question now remains.
I don't really get what you want to do here.
What I think you're trying to do, is to configure and return two cells in the tableView(_:cellForRowAtIndexPath:) method. If you really want to do this, you're totally doing it wrong.
The table view's data source methods asks questions. And your job is to answer those questions by returning a value. For example, numberOfSectionsInTableView(_:) asks you how many sections should there be. An example answer might be return 1, return 10 etc.
Similarly, tableView(_:cellForRowAtIndexPath:) asks
What should be the cell that should be shown in the section and row specified by the index path?
And you answer by returning a UITableViewCell. You can't return two cells because it is asking you to provide a cell to be displayed at that specific section and row. If you gave it two cells to display, how can the table view display them? It doesn't make sense! Each row in the table view can only display one cell!
Therefore, instead of giving tags to the cells, use the indexPath parameter to decide which cell to create.
Let's say you want the first row to display the cell with the identifier "storyCell". And you want the second row to display the cell with the identifier "storyCell2". And your table view has only one section. You can just do this:
switch indexPath.row {
case 0:
reuseIdentifier = "storyCell"
let storyCell1 = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! StoryCell1
storyCell1.theCategory.text = newsItem.storyCategory
storyCell1.theTitile.text = newsItem.titleText
storyCell1.firstParagraph.text = newsItem.paragraph1
return storyCell1
case 1:
reuseIdentifier = "storyCell2"
let storyCell2 = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! StoryCell2
storyCell2.configureCell(newsItem)
return storyCell2
default:
// this shouldn't be reached if you do your other data source methods correctly
return UITabeViewCell()
}
And you should delete these nonsense:
let cells = [tableView.viewWithTag(1), tableView.viewWithTag(2)]
for view in cells {
var reuseIdentifier = "cellID"
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! CustomTableViewCell
It's standard sentence that implements the table view's cell's properties. But Tailor (it's a Swift analyzer/linter) warns about you shouldn't forced the CustomTableViewCell as as! If I used to as as?, I have to implement cell's properties as cell!. But Tailor don't warn about [forced-type-cast] Force casts should be avoided. What's the reason of this? How can I implement cell's without unwrap of cell as cell! What's the correct programming paradigms for forced casts operations in Swift?
I am not familiar with "Tailor" but most likely the reason it is giving you this warning is because if a force cast fails then obviously your program will crash and thats never good.
The as! operator does have its place if you are 100% sure that what you are casting is of that type. But, even then its better to be safe than sorry and you should use a guard or if let statement instead in order to handle a failed cast.
if let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? CustomTableViewCell {
//do what you like with cell
}
or
guard let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? CustomTableViewCell else {
//abort current scope, return, break, etc. from scope.
}
//do what you like with cast cell
I have setup a segue that will show a view controller with a small TableView. I want a different segue to show a bigger TableView but I want the bigger table to have the same exact info as the smaller table. Got the smaller tableView working perfect on its own, but once I give the bigger table a Data source, reset and try it out.....crashes.
// IndexPath or First Cell in TableView
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if self.TaskTableViews.hidden == false {
cell = tableView.dequeueReusableCellWithIdentifier( "FirstTask" , forIndexPath: indexPath) as UITableViewCell!
let list = frc.objectAtIndexPath(indexPath) as! List
cell.textLabel?.text = list.taskName
cell.textLabel?.textColor = UIColor.whiteColor()
TaskTableViews.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
TaskTableViews.layer.cornerRadius = 8
TaskTableViews.separatorColor?.colorWithAlphaComponent(2.0) }
if self.TaskTable2.hidden == false {
cell = tableView.dequeueReusableCellWithIdentifier( "Second Task" , forIndexPath: indexPath) as UITableViewCell!
let list = frc.objectAtIndexPath(indexPath) as! List
cell.textLabel?.text = list.taskName
cell.textLabel?.textColor = UIColor.whiteColor()
TaskTable2.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
TaskTable2.layer.cornerRadius = 8
TaskTable2.separatorColor?.colorWithAlphaComponent(2.0) }
return cell as UITableViewCell
}
The problem is that the code for your two tables is slamming into each other. To fix this, rejigger your logic. Do not make your logic depend on what is hidden. You can handle only one table at a time; just one table is calling here. That table comes in as the tableView parameter. Make your logic depend on that. Depending what table view that tableView parameter is, configure the cell and return it for that table view.
I think that you should consider changing your approach by just resizing the table view dynamically when you go from one scene to the other instead of having two table views if the information is exactly the same.
If you're still pushing for this approach then don't make the condition be that the table view is hidden and instead implement your own logic or boolean to determine this. But again, I'd rather resize a single table view as needed.
You have many problems in your code
1.
var cell = UITableViewCell()
What's the point of this line?
2.
cell = tableView.dequeueReusableCellWithIdentifier( "FirstTask" , forIndexPath: indexPath) as UITableViewCell!
What's the point of casting? This function returns UITableViewCell (not even optional)
3.
cell.textLabel?.text = list.taskName
Should not compile, cause UITableViewCell doesn't have textLabel
4.
TaskTableViews.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
What's the point of doing this every cell request? Move this to viewDidLoad or other appropriate place
Your if { } parts are identical except of reuse identifier
Use tableView argument when needed
Use if { } else
8.
return cell as UITableViewCell
Why cast? Just return
I'm sure I haven't found all of them))