Dynamic DataSource swap for UITableView in Swift - ios

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.

Related

How does iOS call delegates and datasource methods?

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.

Delegate Method to UItableViewCell Swift

I have a Social Network Feed in form UItableView which has a cell. Now each cell has an image that animates when an even is triggered. Now, This event is in form of a string, will be triggered at every cell. the options for the event are defined in another class(of type NSObject).
My issue:
I constructed a protocol delegate method in table view, which will be called whenever the event is triggered for each cell. Then, I define this function in UITableViewCell Class, since my the image will be animating on that.
All is working well but I am unable to figure out how to assign the delegate of TableView class to cell class. What I mean is, how can I use UITableView.delegate = self in cellView class. I have tried using a static variable, but it doesn't work.
I have been playing around the protocols for a while now but really unable to figure out a solution to this.
I hope I am clear. If not, I will provide with an example in the comments. I am sorry, This is a confidential project and I cant reveal all details.
If I understand you correctly, you are trying to make each of your cells conform to a protocol that belongs to their UITableView? If this is the case then this cannot be done. The Delegation design pattern is a one to one relationship, i.e only one of your UITableViewCells would be able to conform to the UITableView's delegate.
Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object—the delegate—and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object.
Quote from the Apple Docs
I would suggest that your UITableViewCell should call a block (Objective-C) or a closure (Swift) whenever your specified event is triggered to achieve what you are looking for. Set up this closure in your tableView:cellForRowAtIndexPath function.
EXAMPLE
TableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCellID", for: indexPath) as! MyTableViewCell
cell.eventClosure = {
//Do something once the event has been triggered.
}
return cell
}
TableViewCell
func eventTriggered()
{
//Call the closure now we have a triggered event.
eventClosure()
}
If I correctly understood your question, maybe this could help:
class ViewController: UIViewController, YourCustomTableDelegate {
#IBOutlet weak var tableView: YourCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.customTableDelegate = self
}
// table delegate method
func shouldAnimateCell(at indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.animate(...)
}
}
}
Try something like this:
Define your delegate protocol:
protocol CustomCellDelegate: class {
func animationStarted()
func animationFinished()
}
Define your CustomCell. Extremely important to define a weak delegate reference, so your classes won't retain each other.
class CustomCell: UITableViewCell {
// Don't unwrap in case the cell is enqueued!
weak var delegate: CustomCellDelegate?
/* Some initialization of the cell */
func performAnimation() {
delegate?.animationStarted()
UIView.animate(withDuration: 0.5, animations: {
/* Do some cool animation */
}) { finished in
self.delegate?.animationFinished()
}
}
}
Define your view controller. assign delegate inside tableView:cellForRowAt.
class ViewController: UITableViewDelegate, UITableViewDataSource {
/* Some view controller customization */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CustomCell.self)) as? CustomCell
cell.delegate = self
cell.performAnimation()
return cell
}
}

Having trouble reloading tableView

I am having trouble reloading the tableView. When my firebaseService.getAllPosts() method runs, it gives me the two posts in the database (as intended). I can tell this because when the didSet print runs, it gives me the correct count. However, because I know the tableview is set before the method gets run, I know I need to reload the tableview to update the count in my datasource. There-in lies the issue. I put the posts variable outside of my class on purpose so it could be accessed from every class. (If this is not a good practice, let me know.)
Where can I run tableView.reloadData() so my tableView datasource updates and gives me the correct could of posts? I've tried putting FeedController().tableView.reloadData() in the didSet and I've tried putting it in viewDidLoad() but neither of these worked. I've also tried adding a variable called _posts and setting it equal to posts and adding didSet to that with tableView.reloadData() inside the didSet but that doesn't work either.
class FeedController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView = UITableView(frame: UIScreen.mainScreen().bounds, style: UITableViewStyle.Plain)
let cellId = "PhotoCell"
let textCellId = "TextCell"
let firebaseService = FirebaseService.sharedInstance
static let sharedFeedInstance = FeedController()
var posts = [Post]() {
didSet {
tableView.reloadData()
print(posts.count)
}
}
override func viewDidLoad() {
super.viewDidLoad()
firebaseService.getAllPosts()
tableView.dataSource = self
tableView.delegate = self
self.view.addSubview(tableView)
}
// MARK: - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
print("sections: \(posts.count)")
return posts.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post:Post? = posts[indexPath.section]
if let _ = post?.imageContentName {
let photoFeedCell = tableView.dequeueReusableCellWithIdentifier(self.cellId, forIndexPath: indexPath) as? FeedTVCellWithPhoto
photoFeedCell?.post = post
return photoFeedCell!
}
let textFeedCell = tableView.dequeueReusableCellWithIdentifier(self.textCellId, forIndexPath: indexPath) as? FeedTVCellText
textFeedCell?.post = post
return textFeedCell!
}
}
Update 1: getAllPosts method from FirebaseService class
func getAllPosts() {
let postRef = ref.child("posts")
postRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
// print(snapshot.value)
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
if let postDictionary = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let post = Post(key: key, dictionary: postDictionary)
FeedController.sharedFeedInstance.posts.insert(post, atIndex: 0)
}
}
}
})
}
In essence, you are addressing the methods and properties of FeedController the wrong way. Every time that you use FeedController() in your code you should be using self.
The following two paragraphs are a refresher about class and instance methods and properties. Feel free to ignore them if it's something that's already in your radar:
If a method or property belongs to an instance (that is, it may vary from instance to instance) you call it with self.property or self.method(), although often you can drop the self. as long as it does not create ambiguity.
If a method or property belongs to the whole class (and therefore it is declared as static let, static var or static func) then you would call it with NameOfTheClass.property or NameOfTheClass.method(). In your case, you would call it FeedController.property or FeedController.method(). That is, without the trailing parentheses. In any case, class methods and properties should be used sparingly and would probably not be appropriate in this situation.
Having settled that, we have the question of how to reload the data. If you move posts inside FeedController (making the array an instance variable) and add tableView.reloadData() (without the self.) to the didSet() of posts, you should be good, notwithstanding other unrelated code smells that others have highlighted and you should try to fix, but those should not affect your ability to reload the data.
So off the bat:
"FeedController().tableView.reloadData()" is
instantiating another FeedController. That's not going to work. Where to put the reload call will depend on your requirements. For example if you have a lot of segues occurring where you go back and forth to this UIViewController and the data is being changed in other UIViewControllers, you can reload in an override of viewWillAppear() or the other lifecycle methods. You need to call that on the tableView for the instance of the UIViewController that is loaded into the UIWindow, not what you're doing now.
Any reason why you're doing a lot of this stuff programmatically rather
than through the storyboard? Either way is fine but I find storyboard
to be a nice separation. For example if you used a UITableViewController through storyboard you could have it set up the delegate and dataSource without doing it through code which just looks like ugly boilerplate. You can set these without using a UITableViewController as well, as long as you meet the protocols. Those things that can be handled in the storyboard should is what I usually go by.
Your class is extending UIViewController and implementing those two protocols, why not just extend a UITableViewController? Unless you're manually putting a UITableView on there and need to show something other than a UITableView on that UIViewController there's no reason I can think of to do what you're doing.
Something else to note is that the way you're doing the PhotoFeedCell and TextFeedCell is kind of weird and gives mixed signals. You're dequeuing a reusable cell and optional casting it and using optional chaining afterwards, both of which implies that you're accepting it may be nil, but then force unwrapping it right after.

change a property from one class in a different class swift (specifically a UILabel)

I'm trying to change the text of a UILabel in a UITableView (a property of one class,) from inside another class, but I'm having trouble. The code looks like this (The problem is at the end, in the didSelectRowAtIndexPath method of myDataSource)
The View Controller
class ViewController: UIViewController
{
//MARK: Properties
#IBOutlet weak var myTableView: UITableView!
#IBOutlet weak var myLabel: UILabel!
//needed so data from myDataSource is retained by ViewController and not just thrown away
let importedDataSource = myDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
myTableView.dataSource = myDataSource()
myTableView.delegate = myDataSource()
}
override func didReceiveMemoryWarning()
{super.didReceiveMemoryWarning()}
}
The UITableViewDataSource and Delegate
class myDataSource: NSObject, UITableViewDataSource, UITableViewDelegate
{
let cellIdentifier = "myTableViewCell"
let myArray = ["Label one", "Label two", "Label three"]
//MARK: TableViewDataSource
func numberOfSectionsInTableView(tableView: UITableView) -> Int
{return 1}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{return myArray.count}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! myTableViewCell
cell.myCellLabel.text = myArray[indexPath.row]
return cell
}
//MARK:TableView Delegate
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
//This Line Here returns the error
ViewController().myLabel.text = "test"
//I want the end product to be ViewController().myLabel.text = myArray[indexPath.row], but left it as "test" just to simplify and isolate the error
}
}
(there's also a UITableViewCell class called myTableViewCell, but I left it out to be shorter)
Running returns "fatal error: unexpectedly found nil while unwrapping an Optional value" upon selecting one of the rows. How should I call myLabel so as to avoid this problem? I've tried hooking it up as an #IBOutlet inside myDataSource by ctrl-dragging it from the storyboard, but as I expected, it only lets me connect it to the view controller.
sorry, the code is a bit confusing with the two similarly named labels. I'm trying to call the myLabel var created in the ViewController (first piece of code), not the myCellLabel created in myTableViewCell (not shown)
The reason you are seeing the message "fatal error: unexpectedly found nil while unwrapping an Optional value" is because you are trying to assign a value to a property of nil.
In the implementation of your UITableViewDelegate method tableView(_:didSelectRowAtIndexPath:) the statement:
ViewController().myLabel.text = "test"
creates a new instance of your ViewController class (that is what ViewController() does)
accesses the myLabel property of the new instance (recall that in your declaration of the class you defined this property to be of type UILabel!, an implicitly-unwrapped optional UILabel, and implicitly-unwrapped optionals begin life as nil until they are assigned a value)
tries to assign "test" to the text property of myLabel - but as we just noted, myLabel is nil and this is why you are receiving the error message
EDIT
In an effort to assist you in your stated goal of "trying to change the text of a UILabel" (that label being the property myLabel of your ViewController instance), the route I would recommend is:
have your ViewController class adopt the UITableViewDelegate protocol
implement your UITableViewDelegate protocol methods inside of your ViewController class, where they will have direct access to the myLabel property
once you've done this, your implementation of that delegate method (now residing inside of your ViewController class) would look like:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
myLabel.text = myTableView.dataSource.myArray[indexPath.row]
}
...and while this will work, even this can be improved - I recommend you check out Apple's Table View Programming Guide
furthermore, unless there is a compelling reason not to, I also recommend that you have your ViewController class act as your UITableViewDataSource - from the Table View Programming Guide: "[t]he class creating the table view typically makes itself the data source and delegate by adopting the UITableViewDataSource and UITableViewDelegate protocols" (see this section)
You need to get the actual table view cell that was tapped in order to access it's variables. Use cellForRowAtIndexPath. It'll look something like this:
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
if let myCell = tableView.cellForRowAtIndexPath(indexPath) as? myTableViewCell {
myCell.myLabel.text = "test"
}
}

DataSource appears to be working but tableView is not reloading

I've got an issue where my tableView isn't updating based on the datasource correctly. I'm using a tabbed application structure with Storyboards.
My overall goal here is to have a tableView on the second tab display items that are removed from an array stored in a struct. The items are added to the array from the first tab.
There are 2 ViewControllers (1 for the interface for scrolling through items and selecting to remove them, and 1 to handle the tableView) and 2 Views (1 for the interface for scrolling through items and removing them and 1 for the tableView). The first tab is for providing the interface for removing the items and the second tab is for the tableView.
The remove and add to the array functionality works, just not the displaying it in the tableView.
Currently, if I hard code items in my "removed items" array, they are displayed in the tableView. The problem is that as I add items to the array from my removeItem function in the first ViewController, the tableView does not update, only the hard coded items are shown.
This makes me assume that I have my datasource and delegate setup correctly, since the tableView is getting it's data from the intended datasource. The issue is it's not updating as the user updates the array with new items.
I've tried using self.tableView.reloadData() with no success. I might not be calling in the correct location though.
I'm not sure where the disconnect is.
Here is my second view controller that controls the tableView
class SecondViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let cellIdentifier = "cellIdentifier"
var removedTopicsFromList = containerForRemovedTopics()
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView?.registerClass(UITableViewCell.self, forCellReuseIdentifier: self.cellIdentifier)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// UITableViewDataSource methods
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return removedTopicsFromList.removedTopics.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier) as UITableViewCell
cell.textLabel!.text = self.removedTopicsFromList.removedTopics[indexPath.row]
return cell
}
Here is the struct where the removed phrases are stored
struct containerForRemovedTopics {
var removedTopics: [String] = []
}
structure instances are always passed by value. So if your code is something like:
var removedTopicsFromList = secondViewController.removedTopicsFromList
removedTopicsFromList.removedTopics.append("SomeTopic")
secondViewController.reloadData()
then you are changing the different structure.
Maybe you got stuck with this problem I guess.

Resources