Programmatically switch UITableView in UITableViewController - ios

NOTE: I know this is a re-post; I posted the same question a few days ago sans-code and, understandably, it was closed. I edited the question to add some relevant snippets but the question was not reopened, so I am reposting here. If this isn't the proper way to do this, let me know!
I have an application with two modes, and the settings screen is slightly different for each mode (one additional section in one of them, a few differences in number of rows, different UserDefaults keys, etc.). In the past I implemented this with a crapton of switch and if statements, but in an effort to make things more maintainable I'm investigating ways on splitting the two modes into their own separate classes of something. Initially I considered making two separate UITableViewController subclasses, however I was having trouble thinking of how that would work with storyboards and such. I then thought to use two separate UITableView subclasses, and select which one to show based on the mode in viewDidLoad.
However, I'm having issues with this approach. I have it set up so that the controller's cellForRow method calls the TableView's cellForRow method, but that's where things break. When trying to do a dequeueReusableCell, the app crashes with the ever vague "EXC_BAD_INSTRUCTION" error on that line.
Here's some relevant code:
ViewController.swift
...
override func viewDidLoad()
{
super.viewDidLoad()
...
tableView = SRScaleSettingsTableView()
}
...
override func tableView(_ tableView: UITableView?, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
return (tableView?.cellForRow(at: indexPath))!
}
SRScaleSettingsTableView.swift
override func cellForRow(at indexPath: IndexPath) -> UITableViewCell?
{
...
switch indexPath.section
{
case 0:
...
let switchCell = dequeueReusableCell(withIdentifier: "SwitchCell") as! SRSwitchCell
^ Debugger breaks on that line with EXC_BAD_INSTRUCTION
...
return switchCell
...
}
}
Any ideas on what would cause this? Is my approach even correct; is there a better way to do this?

You can keep a single UITableView class (you likely don't need to subclass UITableView at all) and a single UIViewController subclass. Create two different classes that implement the UITableViewDataSource protocol (and possibly also UITableViewDelegate). These two classes can implement the various delegate/datasource methods (e.g. cellForRowAtIndexPath, numberOfRowsInSection, didSelectRow) in completely different ways for the different modes your app needs to run in.
protocol SettingsSource: UITableViewDelegate, UITableViewDataSource {
}
class SettingsSourceForModeA: NSObject, SettingsSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)...
}
class SettingsSourceForModeB: NSObject, SettingsSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)...
}
class SettingsViewController: UIViewController {
#IBOutlet tableView: UITableView!
var source: SettingsSource! {
didSet {
tableView.dataSource = source
tableView.delegate = source
}
}
override func viewDidLoad() {
super.viewDidLoad()
// whatever logic is appropriate to determine the mode
if appIsRunningInModeA() {
source = SettingsSourceForModeA()
} else {
source = SettingsSourceForModeB()
}
}
}
The key detail in the above code is the source variable in SettingsViewController - the value of source is based on which mode the app is running in, and it determines what class will be used as the data source for the table view.
The storyboard setup is simple: one scene SettingsViewController, and a single stock UITableView in that scene.
Note that the SettingsViewController above is a UIViewController subclass, not UITableViewController, since the data source and delegate protocols are being implemented in separate classes and determined at runtime. This will require you to manually wire up the tableView outlet in your storyboard. However, you do not wire up the UITableView's dataSource and delegate outlets in the storyboard. Instead, it's done at runtime as shown in the sample code above.
Note that you may not have any need to implement UITableViewDelegate, in which case you can just ignore the references to UITableViewDelegate and its methods in the sample code above. Or, if you the UITableViewDelegate implementation (such as the didSelectRow method) is identical for the two modes your app can run in, you may be able to implement that in your view controller class, in which case you can wire up the delegate outlet of your table view directly to your view controller, in the storyboard.

You have a misunderstanding of how the UITableView and UITableViewController work together. A UITableView needs a UITableViewDataSource to provide it the details of the underlying data (number of sections, number of rows and actual cells, etc). This is what the UITableViewController does (it conforms to the UITableViewDataSource). So if you call the cellForRow for the tableView then it will call it's data sources cellForRow method to obtain that.
So in your code when you do this:
return (tableView?.cellForRow(at: indexPath))!
Your table view calls its data source which is your UITableViewController and that calls the table view cellForRow and so on. You have just entered a recursive loop which is eventually killed off with the error you see.
As for your overall approach I would go down the two UITableViewControllers route as that separates out the different logics between the two making it easier to both understand and maintain and also allowing for more reuse as well.
As for how that works with storyboards it depends greatly on how you switch between the two modes but in essence you can setup segues to switch between the two controllers.

Related

Why does Xcode auto-fix create two methods with the same name `func tableView`?

I am new to swift programming language. I've seen that in creating table in Swift, you have to implement two methods in ViewController class that extends UITableViewDelegate, UITableViewDataSource. What I don't understand is, why does Xcode's auto-fix create two methods with the same name func tableView in this class?
Is this not going to create method overloading or cause bug down the road?
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
let dataArray = ["firt", "second", "third", "four", "five", "six"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video title", for: indexPath)
return videoCell
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.dataSource = self
tableView.dataSource = self
}
}
Even though they have the same function name tableView
They are very different functions.
They both conform to the UITableView delegate and based on its protocol method will affect different functionalities of the tableView.
didSelectRowAt
is not the same as
cellForRowAt
Did Select row at is only triggered when you obviously select a cell
Cell for row at is considered the 'main' tableView function, as this function populates your tableView data cells.
--EDIT
Based on Duncan C comment below.
" the name of your example function is not tableView, The name of of the function is tableView(_:cellForRowAt:) (The parameters are actually part of the function's name, or rather it's function "signature.") "
This is an excellent way to describe the answer.
Edit 2----
Furthermore,
This is very common among programming in swift. The most direct example would be collectionView. It uses almost the identical naming convention.
cellForRowAt
and
didSelectRowAt
There are many other delegate methods that you will encounter with the same situation as you describe in your question.
They are actually different methods. Each one overrides some properties of your TableView.
Just pay attention to the parameters of each function.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
This method creates and configures an appropriate cell for the given index path.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
Here you decide what happens when cell rows are clicked (go to another view, display some content, etc)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
Returns the number of rows each section of your TableView should have.
And so on. There are many others methods that you can use for different reasons. To see which methods you can override, type tableView on your Xcode and see the autocomplete options.
That is called method overloading. Swift designers chose to follow this way of writing functions to make it easier for the programmers to find all related tableView functions. That tableView has many more functions like:
a one that has heightForRowAt and willDisplayCell in its parameters. Swift utilizes heavily a pattern called Delegate Pattern, the framework calls these tableView functions when you assign your ViewController as a delegate.

TableViewCell not being displayed in ViewController

I've come across a very strange issue suddenly in an app I'm building - the tableViewCells in my tableView are not showing up at all, even though the tableView methods are firing (I've checked using print statements within them). The app does not crash, but everything within the table view cell is just not showing up anymore, even though all data variables have been updated.
Below are the tableView methods I've used:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let count = selectedValidInputs.count
return count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = carbonDetailsTable.dequeueReusableCell(withIdentifier: "CarbonDetailsCell") as! CarbonTableViewCell
cell.itemName.text = selectedValidInputs[indexPath.row]
cell.carbonValue.text = carbonValues[indexPath.row]
cell.tip.text = tips[indexPath.row]
cell.hashtag.text = tags[indexPath.row]
return cell
}
Covering some questions that may be asked
I have a Controller that conforms to UIViewController, UITableViewDelegate, UITableViewDataSource, so override methods are not required
I have made sure the identifier for the CarbonTableViewCell (the custom UITableViewCell class) matches the one being used in .dequeueReusableCell method
I have verified all the variable connections from the storyboard to the code
I have not set the number of sections within the tableView, but the code had been working for a month without that too, so don't think the problem is associated with that.
I'm very new to coding altogether, so any help and feedback would be much appreciated!
Note that the page is just empty. Even the 'cellBg' variable below, which is just a view and has a background color, does not show up and neither does the app crash - which is a bit weird.
Edit: Since this is being suggested by everyone, just updating here. On printing the results, I get all values: When the input value is "apple" for example, all the values are shown:
count: 1
selectedValidInputs: ["apple (1 kg)"]
carbonValues: ["550 g CO2, same as 22 plastic bags"]
tips: ["Avoid wastage that adds unnecessary methane to the air with decomposition!"]
tags: ["#very-low-carbon-impact"]
When I add carbonDetailsTable.register(CarbonTableViewCell.self, forCellReuseIdentifier: "CarbonDetailsCell") to viewDidLoad(), I get a crash with the msg shown in attached image
Number of sections is equal 0, ‘selectedValidInputs’ array is empty or you have not placed all of the code that is initialising it with values.
1) Try to implement also
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
2) Try to implement also
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 40.0
}
And comment the estimatedRowHeight and rowHeight setters, to confirm if the cell's autolayout is not broken and table view is unable to determine row height.
(estimatedRowHeight is basically used just for tableView indicator bar and estimating the content size of tableView)
Print your Arrays which you are passing in tableview in CarbonDetailsViewController, and check data is exact as you passed in previous viewcontroller.

How can I reuse a table view in multiple view controllers in Swift?

I'm working on an app right now that requires the exact same table view in multiple scenes on my app, however, the data and the placement of the table will change, as illustrated in these two pictures (table view highlighted in red)
In all instances of the table throughout the app, it should have:
The same delegate methods
The same datasource methods (all though the actual data will change)
The same table view cell types with the same outlets and actions.
The same internal constraints (I don't want to have to manually add constraints for the table cells each and every time).
Right now I'm coding them as two separate table views in two separate view controllers, however, I've realized I will need to replicate the same table and logic in many more places throughout my app and this doesn't feel like the right way to go about. So my question is, how can you accomplish table replication in iOS (using interface builder or swift) that meets the specs above, in a clean and dry way?
If you want to design your cells once and then use them across different table views in different view controllers you have to design them in the separate .xib files. Prototype cells are per view controller and don't scale very well.
If all the data source and delegate methods are the same, you can move implementation of this protocols to the separate class. You can configure this class with the array of items to display.
class ReusableTableView: NSObject, UITableViewDataSource, UITableViewDelegate
{
var tableView: UITableView
var tableViewData: [String]
init(_ tv: UITableView, _ data: [String])
{
tableViewData = data
tableView = tv
super.init()
tableView.delegate = self
tableView.dataSource = self
// Register all of your cells
tableView.register(UINib(nibName: "SomeNib", bundle: nil), forCellReuseIdentifier: "example-id")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableViewData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "example-id", for: indexPath)
}
}
Having this two building blocks you can layout the table view separately on each view controller and wire it up with your reusable data source/delegate.
class ExampleTablewViewController: UIViewController
{
#IBOutlet weak var tableView: UITableView!
var reusableTableView: ReusableTableView!
override func viewDidLoad() {
super.viewDidLoad()
reusableTableView = ReusableTableView(tableView, ["lorem", "ipsum", "dolor"])
reusableTableView.tableView.reloadData()
}
}
You can find a basic example of how I see this in this GitHub repo.

Conforming to UITableViewDelegate and UITableViewDatasource in Swift?

I am learning swift 2.0, and I was wondering if you still need to add the code tableView.datasource = self and tableView.delegate = self like in Obj-C to conform to the protocols?
Example code:
class AboutViewController: UIViewController, UITableViewDataSource, UITableViewDelegate
{
override func viewDidLoad()
{
super.viewDidLoad()
// conform to protocols
aboutTableView.dataSource = self
aboutTableView.delegate = self
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 2
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
return 50
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
// Code here
}
}
Now the table view loads with the correct data in each cell.
However, if I remove aboutTableView.datasource = self and aboutTableView.delegate = self from viewDidLoad, my table view is blank. Why is this?
Is this code still required because I see many youtube tutorials that does not include this anymore in swift, and I'm confused as to why mine doesn't work without it?
First of all, that is completely independent of which language
you use, Swift or Objective-C.
But there are two different cases which may cause the confusion:
A UITableViewController subclass:
UITableViewController already conforms to UITableViewDataSource and UITableViewDelegate. It has a tableView property,
whose dataSource and delegate property are already set to self.
In your subclass, you typically override the
data source and delegate methods.
A UIViewController subclass with a UITableView property:
Here you have defined a UITableView property in your subclass
and initialize it in your code, or
connect it to a table view in the interface builder.
In this case you have to set the dataSource and delegate
property of the tableview, either in code or in the interface
builder, as explained in luk2302's answer.
If data source and delegate are the view controller itself,
then you have to declare the protocol conformance explicitly,
and implement the
data source and delegate methods (but without overriding
a superclass method).
Of course, in both cases, the table view data source and the delegate
can be set to a different object, it does not have to be
the view controller itself.
Yes, some assignment is still required.
Either explicitly via code
OR
What you can do instead is connect them already in the interface builder, making the explicit assignment via code obsolete. That is probably what a lot of tutorials do.

programmatically generate different static UITableviews

I've just joined StackOverflow and i'm struggling with a programming requirement with a iPhone app I'm developing in swift. I have a tableview list of different calculators and i would like to segue to another UITableView when a item is clicked to then do the detailed calculations for that tool.
I am likely to have lots of tools in the first table (>20) and so i don't want to use storyboard to draw up each new UITableView static table with a different segue for each one.
I wonder if anyone can give me some advice on how to programmatically code a presentation of a new UITableViewController with static cells when a item is clicked. I don't want to use storyboard so i would need to use code to both manage the presentation as well as the generation of the next UITableViewController with static cells.
I have been able to program a static tableview with a custom class programmatically and linked to a storyboard UITableViewController item but i want to do all this programmatically and cut out storyboard all together.
Any advice you could offer would be greatly appreciated.
Thanks.
A UITableViewController abstracts some things. It seems like maybe what you want to do is to separate things out and have a little more granular control.
You can do this fairly easily. You need 3 things to make this happen:
UITableView
UITableViewDataSource
UITableViewDelegate
A UITableViewController puts these all together for you. We'll have to create them ourselves.
To do this, we make a View Controller, and inherit UITableViewDataSource and UITableViewDelegate
class Example: UIViewController {
}
// MARK - UITableViewDataSource
extension Example: UITableViewDataSource {
// We need to implement some methods here
}
// MARK - UITableViewDelegate
extension Example: UITableViewDelegate {
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Handle the user clicking an item here
}
}
Three things left to do:
Create and show the table
Get the data to display in the table
Implement the delegate methods
 Creating the table
You should decide if you want to completely programatically create a UITableView, or have Interface Builder lay one out for you in a .xib, and you just link it up via an IBOutlet.
Assuming you want to do the former, you can do the following:
var table: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
table = UITableView(frame: view.bounds)
view.addSubview(table!)
table?.delegate = self
table?.dataSource = self
}
 Get the data
When you push this view controller from your previous view controller, be sure to set a variable on this view controller with your data. Assuming you have an array, it's as simple as something like:
exampleViewController.myData = someArray;
navigationController?.pushViewController(exampleViewController, animated: true)
(be sure to create the myData variable in your View Controller to take this)
Implement the delegate methods
Now we can implement the delegate methods to show the data. You may already be familiar this, but for the sake of completeness:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// You should really use dequeueReusableCellWithIdentifier here.
var cell = UITableViewCell()
cell.textLabel!.text = myData[indexPath.row]
return cell
}

Resources