Send data back to UITableViewController from UITableViewDataSource - ios

I have two different classes that both implement the UITableViewDataSource and UITableViewDelegate protocols. They are separate from my UITableViewController.
I would like to choose the correct data source class to instantiate in viewDidLoad() and then set UITableViewController to be a delegate of UITableViewDataSource and UITableViewDelegate classes. (I return an object from these classes to UITableViewController for prepareForSegue to know what to display in the detail view controller screen.)
This doesn't work.
At runtime it breaks without a runtime error, just with "Thread 1: EXC_BAD_ACCESS (code=1, address=...) the line "class AppDelegate: UIResponder, UIApplicationDelegate {"
However, if I define the data source object as an instance variable in the UITableViewConroller (as opposed to doing it within viewDidLoad()) then it works. Of course, this defeats the purpose, since now I can't switch to another data source.
It seems that if I want to set UITableViewController as a delegate (i.e., want to be able to send data back from data source) then I can't do this in viewDidLoad() for some reason. Maybe it hasn't finished creating the objects yet? (Everything works if I create objects as instance variables and immediately initialise them.)
protocol GroupByDelegator {
func callSegueFromGroupByDelegator()
}
class RemindersViewController: UITableViewController, GroupByDelegator {
#IBOutlet var remindersTableview: UITableView!
// var dataSource = GroupByNothingDataSource() // THIS WORKS, BUT THEN I CAN'T CHANGE THE DATASOURCE ANYMORE
var reminderWrapperToBeDisplayedInDetailView: ReminderWrapper?
override func viewDidLoad() {
super.viewDidLoad()
// if ... {
var dataSource = GroupByNothingDataSource() // BREAKS THE CODE
// } else {
// var dataSource = GroupByPriorityDataSource()
// }
dataSource.groupByDelegator = self // used so that the datasource can call the callSegueFromGroupByDelegator() func that will pass an object back to here.
self.tableView.dataSource = dataSource
self.tableView.delegate = dataSource
}
...
// This function is called by the data source delegates (GroupByNothingDataSource, GroupByPriorityDataSource) because they can't perform segues.
func callSegueFromGroupByDelegator(reminderWrapper: ReminderWrapper?) {
reminderWrapperToBeDisplayedInDetailView = reminderWrapper
//try not to send self, just to avoid retain cycles
self.performSegueWithIdentifier("reminderDetail", sender: tableView)
}
}
class GroupByPriorityDataSource: NSObject, UITableViewDataSource, UITableViewDelegate, TableViewCellDelegate, RemindersViewControllerDelegate {
var groupByDelegator: GroupByDelegator!
...
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
...
// Pass object back to UITableViewController
self.groupByDelegator.callSegueFromGroupByDelegator(reminderWrapper?)
}
}

I ended up initializing both datasources as class instance variables (instead of initializing only one dynamically depending on the user interaction.) Now I choose between the already initialized datasources dynamically depending on the user interaction.
It solved my problem, but hasn't really addressed the technical issue. Is this a bug in the platform?

Related

what is the function of NSObject in StoryBoard / Interface Builder?

I am currently following a video tutorial course about test driven development of iOS in Swift, but when testing Table View in View Controller, I get stuck, since I don't understand why we need NSObject in Interface builder like the picture below:
Movie Library Data Service is inheritted NSObject class:
the class of MovieLibraryDataService is like this:
import UIKit
class MovieLibraryDataService: NSObject, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
and the MovieLibraryDataService class will be used in XCTestCase is like this :
#testable import FilmFest
class LibraryViewControllerTests: XCTestCase {
var sut: LibraryViewController!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
sut = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LibraryViewControllerID") as! LibraryViewController
_ = sut.view
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
// MARK: Nil Checks
func testLibraryVC_TableViewShouldNotBeNil() {
XCTAssertNotNil(sut.libraryTableView)
}
// MARK: Data Source
func testDataSource_ViewDidLoad_SetsTableViewDataSource() {
XCTAssertNotNil(sut.libraryTableView.dataSource)
XCTAssertTrue(sut.libraryTableView.dataSource is MovieLibraryDataService)
}
// MARK: Delegate
func testDelegate_ViewDidLoad_SetsTableViewDelegate() {
XCTAssertNotNil(sut.libraryTableView.delegate)
XCTAssertTrue(sut.libraryTableView.delegate is MovieLibraryDataService)
}
// MARK: Data Service Assumptions
func testDataService_ViewDidLoad_SingleDataServiceObject() {
XCTAssertEqual(sut.libraryTableView.dataSource as! MovieLibraryDataService, sut.libraryTableView.delegate as! MovieLibraryDataService)
}
}
and the definition of LibraryViewController:
import UIKit
class LibraryViewController: UIViewController {
#IBOutlet weak var libraryTableView: UITableView!
#IBOutlet var dataService: MovieLibraryDataService!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.libraryTableView.dataSource = dataService
self.libraryTableView.delegate = dataService
}
}
I really don't understand why I need to make that MovieLibraryDataService class
I usually use:
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
but why do I need to write :
self.libraryTableView.dataSource = dataService
self.libraryTableView.delegate = dataService
MovieLibraryDataService is just another class which implements UITableViewDataSource and UITableViewDelegate, with the difference that the storyboard instantiates it and that storyboard-created instance is bound via the IBOutlet #IBOutlet var dataService: MovieLibraryDataService!. All objects in the storyboard are storyboard-created, that’s why one has to bind them to variables to use if they are not bound to other variables you use already.
Naming the variable dataService is just a fancy way of saying that it is supposed to serve, in this case as dataSource and delegate for a tableView, since it implements those delegate-protocols.
Since the dataService is instantiated by the storyboard, you could try if you can bind the dataService inside the storyboard to tableView. This is possible because you have the dataService referencable in the storyboard. This would replace setting dataSource and delegate in the viewController.
Another example
AppDelegate is also an NSObject inside the Storyboard so the UIApplication/NSApplication inside the storyboard is able to reference that to use, avoids having to set up AppDelegate yourself outside a storyboard. (Would otherwise be ugly, maybe that’s macOS only though, since mac apps have to show a Menu even if its empty.)
Edit
You can use NSObject on the Storyboard for different purposes, one of them could be also the delegation. Instead of setting it programatically like:
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
you can hold control and then set the corresponding delegates like below:
Original Answer
Because MovieLibraryDataService that conforms to UITableViewDataSource and UITableViewDelegate and not your LibraryViewController.
If you want to change it as you are always used to, change your code to:
class LibraryViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var libraryTableView: UITableView!
var dataService = MovieLibraryDataService()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
Personally I would suggest to keep it as you have it, as View Controllers tend to grow and become like we call it Massive View Controller

How To Bind An Array Of Custom Objects Using Swift 3, ReativeKit, and Bond

Background
I have an IOS application that can receive a real-time stream of data. I have implemented custom value objects to store/capture this data from a live stream. I now need to bind my custom data objects to the UI (mostly use tableviews and custom cell's that call into those custom object values).
Question
How can I bind an array of custom object values to my UI using Bond, ReactiveKit, or other framework in Swift 3?
Example Code
public class Device {
var name: String
var status: String
}
public class DeviceController {
var devices = Array<Device>()
// more code to init/populate array of custom Device classes
}
public class CustomViewController: ... {
var deviceController = DeviceController()
var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// more code to register custom cell
}
public class CustomCell:UITableviewCell {
#IBOutlet weak var deviceName: UILabel!
#IBOutlet weak var deviceStatus: UILabel!
}
You use the delegate pattern, which is already set up for many UIKit elements including UITableView.
A UITableView has two properties that can be any object conforming to two protocols, specifically
var dataSource: UITableViewDataSource?
var delegate: UITableViewDelegate?
So for your UITableView, you assign an object to act as the dataSource and delegate. Often, but not always, one would make the containing ViewController both the dataSource and delegate.
override func viewDidLoad() {
tableView.dataSource = self
tableView.delegate = self
...
}
However, you first have to make the ViewController conform to those protocols.
public class CustomViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
Command click on the two conformance declarations and you'll be able to see the methods you have to add to your view controller to conform. They're pretty obvious what they do, and you'll probably be able to figure it out from there.
But specifically you need to add the numberOfRows, numberOfSections and this method, which is I think the one you're asking about.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// dequeue a cell for reuse
// type cast the cell to your UITableViewCell subclass
// set the device name and device status labels like so..
cell.deviceName = deviceController.devices[indexPath.row].name
cell.deviceStatus = deviceController.devices[indexPath.row].status
return cell
}
From there, the tableView will automatically request the data when the subviews are laid out. If your data isn't all available instantly, you can call tableView.reloadData() when it is.

Dynamic DataSource swap for UITableView in Swift

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.

What is the best design solution for this situation in iOS?

I have UITableView with two static cells. Each cell has custom class and independently validate account name, when I fill text field in the cell. (This part of code I got as is and I am not allowed to rewrite it). The cell delegates about changes if validation is correct to delegate (SocialFeedSelectCellDelegate). Originally, this tableView appeared in SignUpViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate, SocialFeedSelectCellDelegate only.
Problem : The same UITableView should appear in two different places (SignUpViewController and SettingsViewController). Also SignUpViewController and SettingsViewController should know about success or fail of account validation.
What I tried : I created SocialFeedTableViewController: UITableViewController, SocialFeedSelectCellDelegate for the tableView with two cells. Set view in SocialFeedTableViewController as container view for SignUpViewController and SettingsViewController. I used second delegation (from SocialFeedTVC to SignUp and Settings) to notify SignUp and Settings about validation changes. I think it is bad idea, because of double delegation. Teammate said me that it is hard to understand.
Question: What is the best and simple design solution for the problem?
Why is the double delegation a problem? As far as I see it you have 2 table views, 1 for each controller. Then each controller sets the delegate to each of the table view as self. Even if not it is quite common to change the delegate of the object in runtime. It is also normal to have 2 delegate properties with the same protocol simply to be able to forward the message to 2 objects or more.
There are many alternatives as well. You may use the default notification center and be able to forward the messages this way. The only bad thing about it is you need to explicitly resign the notification listener from the notification center.
Another more interesting procedure in your case is creating a model (a class) that holds the data from the table view and also implements the protocol from the cells. The model should then be forwarded to the new view controller as a property. If the view controller still needs to refresh beyond the table view then the model should include another protocol for the view controller itself.
Take something like this for example:
protocol ModelProtocol: NSObjectProtocol {
func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?)
}
class DelegateSystem {
class Model: NSObject, UITableViewDelegate, UITableViewDataSource, ModelProtocol {
// My custom cell class
class MyCell: UITableViewCell {
weak var modelDelegate: ModelProtocol?
var indexPath: NSIndexPath?
func onTextChanged(field: UITextField) { // just an example
modelDelegate?.cellDidUpdateText(self, text: field.text) // call the cell delegate
}
}
// some model values
var firstTextInput: String?
var secondTextInput: String?
// a delegate method from a custom protocol
func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?) {
// update the appropriate text
if cell.indexPath?.row == 0 {
self.firstTextInput = text
} else {
self.secondTextInput = text
}
}
// table view data source
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = MyCell() // create custom cell
cell.indexPath = indexPath // We want to keep track of the cell index path
// assign from appropriate text
if cell.indexPath?.row == 0 {
cell.textLabel?.text = self.firstTextInput
} else {
cell.textLabel?.text = self.secondTextInput
}
cell.modelDelegate = self // set the delegate
return cell
}
}
// The first view controller class
class FirstViewController: UIViewController {
var tableView: UITableView? // most likely from storyboard
let model = Model() // generate the new model
override func viewDidLoad() {
super.viewDidLoad()
refresh() // refresh when first loaded
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
refresh() // Refresh each time the view appears. This will include when second view controller is popped
}
func refresh() {
if let tableView = self.tableView {
tableView.delegate = model // use the model as a delegate
tableView.dataSource = model // use the model as a data source
tableView.reloadData() // refresh the view
}
}
// probably from some button or keyboard done pressed
func presentSecondController() {
let controller = SecondViewController() // create the controller
controller.model = model // assign the same model
self.navigationController?.pushViewController(controller, animated: true) // push it
}
}
// The second view controller class
class SecondViewController: UIViewController {
var tableView: UITableView? // most likely from storyboard
var model: Model? // the model assigned from the previous view controller
override func viewDidLoad() {
super.viewDidLoad()
refresh() // refresh when first loaded
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
refresh() // Refresh each time the view appears. This will include when third view controller is popped
}
func refresh() {
if let tableView = self.tableView {
tableView.delegate = model // use the model as a delegate
tableView.dataSource = model // use the model as a data source
tableView.reloadData() // refresh the view
}
}
// from back button for instance
func goBack() {
self.navigationController?.popViewControllerAnimated(true)
}
}
}
Here the 2 view controllers will communicate with the same object which also implements the table view protocols. I do not suggest you to put all of this into a single file but as you can see both of the view controllers are extremely clean and the model takes over all the heavy work. The model may have another delegate which is then used by the view controllers themselves to forward additional info. The controllers should then "steal" the delegate slot from the model when view did appear.
I hope this helps you understand the delegates are not so one-dimensional and a lot can be done with them.

Swift - Override functions in UIViewController vs UITableViewController

When I use the UITableViewController to create a tableView, you get a lot of override functions, but when you use a regular UIViewController you get an error when using these same override functions and you are forced to change them to regular functions. I believe this is why my core data won't load into my cells, and tried to use the viewDidLoad function to get my data to load.
I know my code should work since all I'm trying to do is transfer all my code from a UITableViewController to a UIViewController, and my code worked in my UITableViewController.
My effort so far:
override func viewDidLoad() {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
let CellID:NSString = "CELL"
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier(CellID) as UITableViewCell
if let ip = indexPath as Optional {
var data:NSManagedObject = myList[ip.row] as NSManagedObject
cell.textLabel!.text = data.valueForKeyPath("username") as? String
}
return cell
}
}
Are the override functions the reason my cells are empty, or are there other aspects when using a regular UIViewController to show a tableView?
Any suggestions would be appreciated.
(1) You have to add UITableViewDelegate to the class in order to access the delegate methods, ex:
class ViewController: UIViewController, UITableViewDelegate {
After adding the UITableViewDelegate to the class, your UITableView delegate functions should auto-complete.
Also, make sure to set the UITableView's delegate to self in order to have the delegate methods populate the table.
(2) Right now, your cellForRowAtIndexPath method is within your viewDidLoad. Move it so it's not contained within any other method.

Resources