in the UIViewController's viewDidLoad() there is a method being called for updating a class variable. Once I navigate to another view controller and come back to this one, UITableView's delegate methods are being called first where that class variable is being used. App is crashing because that variable is being constructed in viewDidLoad(). How can this issue be fixed? Thank you.
the class variable in question:
var showHideDict = Dictionary<Int, Bool>()
the viewDidLoad():
override func viewWillAppear() {
super.viewWillAppear()
makeAPICall()
}
the API calling method:
func makeAPICall() {
// create helper object
let helper = ModelHelper()
helper.createModels(receivedDict: result! as NSDictionary)
// store the showHideDict
for index in 0...(Modelclass.models.count - 1) {
self.showHideDict[index] = true
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
In your UITableViewDelegate methods I think you'll want to check whether the data in your dictionary is populated as you expect it to be. For instance, this is what I frequently do in my code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return tableData?.count ?? 0
}
Which is saying: if tableData is not nil, then return the number of elements in the array. Otherwise return 0
Then in your cellForRowAt method, just conditionally check whether the data in your showHideDict is populated as you expect, if not don't force unwrap. Swift enforces safety with optionals... work with it not against it.
Once your API call is completed, all you need to do is do tableView.reloadData() and the tableview will be constructed with your newly-populated data.
Related
Getting a
Thread 1: Fatal error: Index out of range
error when running an app on iPad - it works on iPhones.
In a UITableView in a Master-Detail app, I've got the numberOfRowsInSection returning the count on an array, and apparently it's not getting initialized before the call. Here is the relevant code:
var myObjects = [[MyObject]]()
var first = [MyObject]()
var second = [MyObject]()
let sectionTitles = ["First", "Second"]
override func viewWillAppear(_ animated: Bool) {
...
loadMyObjects()
...
}
private func loadAmenities() {
myObjects.removeAll()
guard let myObject1 = new MyObject(name: "One", property2: "initialized_property") else {
fatalError("Failed to initialize myObject1")
}
...
first += [myObject1, myObject2, myObject3, myObject4]
second += [myObject5, myObject6, myObject7]
myObjects = [first, second]
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sectionTitles.count
}
override func tableView(_ tableView:UITableView, numberOfRowsInSection section: Int)-> Int {
return myObjects[section].count // error happens here
}
Where should I be instantiating the data before the table tries to load? This is weird in that it is the tableView causing the issue, but the tableView loads properly in iPhone. Thanks for any help.
Edit:
On iPhone, it appears that the viewWillAppear method gets called before the tableview's numberOfRowsInSection, but in iPad, it's the opposite order. In fact, the viewDidLoad and tableView methods are called before viewWillAppear
numberOfSections needs to return myObjects.count. The tableview is requesting data before you have loaded data into the myObjects array, but sectionTitles is initialised when the VC is instantiated.
override func numberOfSections(in tableView: UITableView) -> Int {
return myObjects.count
}
You will also need to reload the table once you have added the data.
The reason the crash only occurs on iPad is due to the different behaviour of the master-detail controller on an iPad.
The viewWillLoad method was not being called in iPad, and since the initializations of the data arrays are all local data and there's no need for asynchronous loading, the easiest workaround was to test an array and call the loading function in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
...
if winter.count < 1 {
loadAmenities()
}
}
On the click of a create button, I navigate to tableviewcontroller screen. But nothing is populated in the tableview since the array which populates the tableview hasn't been called yet.
Now once the tableview screen is reached, after a few seconds another method is called elsewhere(in another file), which in turn calls a function in this tableviewcontroller screen. This is that method of the tableviewcontroller screen which is called...
func stopIndicator(thegrpName: String) {
stopIndicator()
let realm = try! Realm()
let chatGrp = realm.objects(ChatGroup.self)
chatGroup = chatGrp
tableview.reloadData() //CRASH HAPPENS HERE
}
In this method, once I reach tableview.reloadData() it crashes with the error Unexpectedly found nil while unwrapping an optional value..
I referred this link which seems to have a similar problem...but couldn't get much help out of it...
What could be the reason for this crash...?
EDIT 1: The numberOfRows and cellForRowAt.. is given like so...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let chatGrp = chatGroup {
return chatGrp.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: ChatGroupTableViewCell = tableView.dequeueReusableCell(withIdentifier: "chatgroupIdentifier") as! ChatGroupTableViewCell
let groupChatObj = chatGroup![indexPath.row]
cell.chatLabel.text = groupChatObj.lastMessage?.text?.text
return cell
}
It looks you are trying to create delegate method but in another file where you are trying to call delegate method stopIndicator you are calling method on singleton instead which gives you an error.
So, set delegate right. First create protocol
protocol YourProtocol {
func stopIndicator(thegrpName: String)
}
then in another file create delegate property
var delegate: YourProtocol?
now when you need to call delegate method stopIndicator call this
delegate?.stopIndicator(thegrpName: ...)
now add to your ViewController protocol
ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, YourProtocol
and now somewhere set your another file class delegate as your ViewController (if its view set it in viewDidLoad if it is another ViewController set it in prepareForSegue)
fileClass.delegate = self
For example, when we are creating tableview we need some datasource methods like
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messageArray.count
}
I don't call this anywhere. However, iOS does this instead of me and I wonder how iOS does this?
iOS search for a tableview, if it is available on the view then call delegates and datasource methods or it called when we declare uiTableView.delegate = self or uiTableView.datasource = self.
Another is these methods called before viewDidLoad?
Generally speaking, this is the setup for a class with a delegate:
class SimpleTableView {
var delegate: SimpleTableViewDelegate?
// ...
func renderCell(at row: Int) { // called whenever the table needs to render a cell
let cell = SimpleTableViewCell()
cell.frame.size.height = delegate?.tableView(self, cellHeightForRow: row)
// continue rendering cell
}
}
The protocol SimpleTableViewDelegate contains the delegate methods. It would look something like this:
protocol SimpleTableViewDelegate {
func tableView(_ tableView: SimpleTableView, cellHeightForRow: Int) -> CGFloat
}
So what we have here is a class, SimpleableView, that gets data from somewhere (the view controller). This is how the delegate comes into play:
class ViewController: UIViewController, SimpleTableViewDelegate {
var tableView = SimpleTableView()
override func viewDidLoad() {
tableView.delegate = self
}
func tableView(_ tableView: SimpleTableView, cellHeightForRow: Int) -> CGFloat {
return 44
}
}
This is essentially how a delegate works, and that is what the real tableView is doing. You set the tableView delegate and tableView calls the delegate methods to get information from you.
Hopefully this helps explain to you how the delegate works here, what calls it, and what's going on in general. If you need clarification, don't hesitate to ask!
So a quick way to look at this is separate these two things.
First lets look at delegate
https://developer.apple.com/documentation/uikit/uitableviewdelegate
The delegate provides a set of methods that you can include in your code that provides a callback for uitableview to execute certain protocol defined methods depending on whats happening within the tableView.
example func tableView(UITableView, heightForRowAt: IndexPath)
This example allows uitableview to ask you how should i display a certain cell at this current indexpath.
Next lets look at the datasource
https://developer.apple.com/documentation/uikit/uitableviewdatasource
Datasource works in a similar fashion as the delegate but provides a different set of methods that help you populate your table view.
example func numberOfSections(in: UITableView)
Apple's uitableview will call this method and ask the tableview how many sections should I display.
Ultimately, these are just protocols that allow the tableview to interact with your code and helping you display your table with your configuration!
tableView:numberOfRowsInSection is method of the UITableViewDatasource protocol. The methods of data source are called by method reloadData() of UITableView.
According to documentation of UITableView:
UITableView overrides the layoutSubviews() method of UIView so that it
calls reloadData() only when you create a new instance of UITableView
or when you assign a new data source. Reloading the table view clears
current state, including the current selection. However, if you
explicitly call reloadData(), it clears this state and any subsequent
direct or indirect call to layoutSubviews() does not trigger a reload.
I've been searching for awhile without luck. I am trying to find an example of a View Controller with a UITableView that has sections. The examples I've see are all dealing with a Table View Controller which I cannot use as I have need of buttons in the same view which control the content of the table view. Anyone have an example, know of an example or have an idea about to implement such? Thanks.
Edit
I've got a table view in a view controller, get the data from an api call, separate the sections and data in an array of a struct. I then send this to be bound to the table view. Doing so throws
[UIView tableView:numberOfRowsInSection:]: unrecognized selector sent to instance
but I don't understand where the problem is.
Code for the tablview
//MARK: Tableview delegates
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let count = incidentDataSection?.count{
return count
}
return 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (incidentDataSection?.count)! > 0{
return incidentDataSection![section].incidents.count
}
return 0
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return incidentDataSection?[section].title
}
/*
func tableView(tableView: UITableView, iconForHeaderInSection section: Int) -> UIImage? {
return incidentDataSection?[section].icon
}*/
//if clicked, will openn details view passing in the details
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//let incidentDetails = incidentData?[indexPath.row]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let section = incidentDataSection?[indexPath.section] {
let cell = tableView.dequeueReusableCell(withIdentifier: "IncidentTableViewCell") as! IncidentTableViewCell
cell.roadNameLabel.text = section.incidents[indexPath.row].RoadWay
cell.whenLabel.text = section.incidents[indexPath.row].DateCreated
cell.statusLabel.text = section.incidents[indexPath.row].DateCleared
return cell
}
return UITableViewCell()
}
incidentDataSection is an array of a struct which has the section title and the different items.
Answer
Though I received some fairly good feedback, the cause was actually a typo. Looking closely at
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return incidentDataSection?[section].title
}
you'll notice the problem is that there is no underscore before tableView:. What was happening is that the datasource and delegate were skipping over the functions since with and without call different protocols in swift 3. Thanks to thislink I was able to figure out the cause. My bad for forgetting to mention this was in Swift 3. Might had saved everyone some time.
You need a tableview instance in your view controller.
Implement the protocols UITableViewDelegate, UITableViewDataSource in your view controller as a UITableViewController.
Don't forget bind the tableview in XIB with tableview in the class.
Look this sample:
class Sample01ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
tableView?.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.tableView?.reloadData()
}
// ...
You have the required methods implemented, however it sounds like you need to "subclass" or "subcribe" to the UITableView's delegate and dataSource. By using:
class MyViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView : UITableView!
}
Now that you have those protocols you will need to set your tableView's delegate and dataSource to your viewController. You can do this using storyboard by drag and drop, or inside of your viewDidLoad() which is what I always do because it is easy for other developers to see from the start of opening your code where your delegate and dataSources are assigned to. Using:
#override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Then your delegate methods and dataSource methods in your viewcontroller will be called for that tableView. Then you can add the IBOutlets to UIButton/UILabel/UISwitch, etc... and do what you will with your ViewController without being limited to simply using a table view inside of that view controller. I Almost always use this methods when using UITableViews/UICollectionViews even if I set the tableView/collectionView to be the size of the whole view because I like the freedom of using a UIViewController over a UITableViewController/UICollectionViewController.
*Note numberOfRows() is not required but I always override it as well, just kind of a habit at this point. Also you sound new to iOS development, so if you aren't already, the next thing I would look into after getting your tableView up and running is pulling your data from your API on a background thread to keep your mainThread open for user response on your UI, DispatchQueue. This is really important if you are displaying images from the API.
I am new to iOS/Swift development, and having a problem with making a dynamic swap of DataSource work for a UITableView - note I am not swapping the Delegate, just the DataSource.
I have read other similar questions/responses on Stack Overflow, and not found one that's relevant to my situation. Typically they're about setting the DataSource on "viewDidLoad" (e.g. this one, and this one), whereas my situation is about swapping the DataSource when the user presses a button. The problems in the referenced questions don't exist in my code.
Here's outline of my code. I have the buttonPress method connected to the TouchUpInside event in the storyboard:
class ViewController: UIViewController, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
...
#IBAction func buttonPress(sender: UIButton) {
...
self.tableView.dataSource = DummyDataSource()
self.tableView.delegate = self
self.tableView.reloadData()
}
...
}
...and here's my datasource class:
import UIKit
class DummyDataSource: NSObject, UITableViewDataSource {
let names = ["A", "B", "C"]
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(simpleTableIdentifier) as UITableViewCell?
if ( cell == nil ) {
cell = UITableViewCell( style: UITableViewCellStyle.Default,
reuseIdentifier: simpleTableIdentifier)
}
cell!.textLabel?.text = names[indexPath.row]
return cell!
}
}
When I press the button, I can see that the pressButton method is being called correctly, but the data doesn't show up in the tableView (no errors - just no data). Any ideas please? Thank you.
UITableView's dataSource property is either unsafe_unretained or weak, depending on which version of iOS. Either way, as with any other delegate, it doesn't keep a strong reference.
So when you write a line like this:
self.tableView.dataSource = DummyDataSource()
Your newly instantiated DummyDataSource() property doesn't have any strong references pointing to it. It is therefore immediately released by ARC.
We need to keep a strong reference to the data source if we want it to stick around.
My recommendation would be to add a data source property to your view controller which can keep the strong reference. We will also use the didSet of this property to set the table view's data source property and reload its data.
var dataSource: UITableViewDataSource? {
didSet {
tableView?.dataSource = dataSource
tableView?.reloadData()
}
}
We use optional-chaining to protect against the data source being set before the view is loaded and the tableView property is populated. Otherwise, we will get a fatal error for trying to unwrap nil.
We shouldn't need to be setting the data source property on the table view anywhere else. And the only reason why we should need to called reloadData() anywhere else is if our data source itself can change the data it is representing. However, it is important that reloadData() is called in sync with resetting the dataSource to protect against some likely index-out-of-bound crashes.