This might sound like an odd question but I'm trying to implement the BEMSimpleLineGraph library to generate some graphs that I have place in a UITableView. My question is how I reference an external dataSource and Delegate to have different graphs placed in each cell (BEMSimpleLineGraph is modelled after UITableView and UICollectionView). I currently have something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: FlightsDetailCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as FlightsDetailCell
cell.userInteractionEnabled = false
if indexPath.section == 0 {
cell.graphView.delegate = GroundspeedData()
cell.graphView.dataSource = GroundspeedData()
return cell
}
if indexPath.section == 1 {
cell.graphView.delegate = self
cell.graphView.dataSource = self
return cell
}
return cell
}
My dataSource and Delegate for section 1 is setup properly below this and the GroundspeedData class looks like this:
class GroundspeedData: UIViewController, BEMSimpleLineGraphDelegate, BEMSimpleLineGraphDataSource {
func lineGraph(graph: BEMSimpleLineGraphView!, valueForPointAtIndex index: Int) -> CGFloat {
let data = [1.0,2.0,3.0,2.0,0.0]
return CGFloat(data[index])
}
func numberOfPointsInLineGraph(graph: BEMSimpleLineGraphView!) -> Int {
return 5
}
}
For some reason when I run the app, Xcode reports that it cannot find the dataSource for section 0, specifically "Data source contains no data.". How should I otherwise reference this alternate dataSource?
cell.graphView.delegate = GroundspeedData()
cell.graphView.dataSource = GroundspeedData()
One problem is: the delegate and data source are weak references. That means they do not retain what they are set to. Thus, each of those lines creates a GroundspeedData object which instantly vanishes in a puff of smoke. What you need to do is make a GroundspeedData object and retain it, and then point the graph view's delegate and data source to it.
Another problem is: do you intend to create a new GroundspeedData object or use one that exists already elsewhere in your view controller hierarchy? Because GroundspeedData() creates a new one - with no view and no data. You probably mean to use a reference to the existing one.
Related
With the inspiration coming from the idea that you can code anything, i tried my hand at a complicated CollectionView nested structure which goes as follows:
CustomCollectionViewController
--CustomCollectionViewCell
----CustomTableView
------CustomTableViewCell
--------CustomPickerView
In CustomCollectionViewController, the main data feed comes from property:
var cardFeed: [String: [Card]] = [:]
Card is my defined model and variable cardFeed is applied the usual way in UICollectionView Delegate & DataSource methods:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "card", for: indexPath) as! CustomCollectionViewCell
cell.contentView.clipsToBounds = true
cell.delegate = self
cell.card = self.cardFeed[string]![indexPath.row]
cell.cardIndex = indexPath.row
}
From the delegate methods, cardFeed above sets a main property in CustomCollectionViewCell used to update the interface:
var card: Card! {
didSet{
setupCard()
}
}
The property card is also the data feed for UITableView Delegate & Datasource.
Everything works perfectly, everything shows up as it should. Except for the fact that when a user picks values from the CustomPickerView, the main data feed, namely cardFeed defined in CustomCollectionViewController (shown above) must update!
My solution is this:
(1) Given that there are three components, define array that records changes in CustomPickerView selected rows and a call back method to pass down the variable:
var selectedRow: [Int] = [1, 0, 0] {
didSet {
if updateRow != nil {
updateRow!(self.selectedRow)
}
}
}
var updateRow: ( ([Int]) -> () )?
(2) In CustomCollectionViewCell define another call back with an extra argument, to keep track of what cell actually sent the selected row array :
var passSelectedRow: (([Int], Int) -> ())?
which is called in tableViews cellForRowAtIndexPath method:
cell.updateRow = { selectedRow in
self.passSelectedRow!(selectedRow, indexPath.row)
}
(3) finally update cardFeed in CustomCollectionViewController cellForItemAtIndexPath:
cell.passSelectedRow = { selectedRow, forIndex in
if self.cardFeed[string]![indexPath.row].chosenFood[forIndex].selectedRow != selectedRow {
self.cardFeed[string]![indexPath.row].chosenFood[forIndex].selectedRow = selectedRow
}
}
But here is the problem, if i now add a didSet to cardFeed, it will create an infinite loop because cellForRowAtIndexPath will be called indefinitely. If i get the CustomCollectionViewCell reference anywhere other than cellForItemAtIndexPath, self.collectionView?.reload() does not work! Is there a way i can update my variable cardFeed in CustomCollectionViewController from the selected rows in CustomPickerView?
When communicating between objects it is bad practice to make the child object have a strong reference to its owner, that is how you end up with retain cycles and bugs.
Let's take a look at the two most common ways of communicating between objects: delegation and notification.
with delegation:
Create a protocol for communicating what you want, in your example:
protocol PickerFoodSelectedDelegate : class {
func selected(row : Int, forIndex : Int)
}
Add weak var selectionDelegate : PickerFoodSelectedDelegate as a variable in the picker class
In the tableView class, during cellForItemAtIndexPath, you assign self to picker.selectionDelegate
You then create a similar structure for communicating between the table and the collection view.
The key part is that delegate references be declared as weak, to avoid retain cycles and bugs.
With notifications you can use NotificationCenter.default to post a notification with any object you want, in this case you would:
Subscribe to a notification name you choose in the table view.
Post a notification from the picker view when an option is chosen.
When the table receives the notification, extract the object.
Do the same from the table to the collection view.
Hope this helps!
I have a UITableView with 2 sections each of which contains one cell. These cells contain UICollectionViews. These collection views are of different types. Trying to follow the MVC design pattern, I make my ViewController a data source and a delegate of both UITableView and UICollectionView.
Here is the data source code of my table view:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: recomendationCellId, for: indexPath) as! RecomendationsTableViewCell
cell.collectionView.dataSource = self
cell.collectionView.delegate = self
return cell
} else if indexPath.section == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: collectionCellId, for: indexPath) as! CollectionsTableViewCell
cell.collectionView.dataSource = self
cell.collectionView.delegate = self
return cell
} else {
return UITableViewCell()
}
}
Now, I need to dequeue cells for the collection view, by using collectionView(_:,cellForItem:) method. Inside of it I need to check the section of the table view to dequeue the right cell.
The problem is that I know that it should have a simple solution but can't figure it out. Do you have any suggestions how this can be achieved?
If collection view is inside cell then handle it with in cell only...so when you are setting cell.collectionView.dataSource = self it means you are setting delegate to current view controller not the cell, means that collection view look for the data source in current view controller.
As you are using MVC its better to implement a datasource and delegate methods inside cell.
So you need to ultimately set cell.collectionView.dataSource = cell as cell will implement data source and delegate methods.
Your MVC pattern won't break even if you confirm to the collection view delegates and datasource methods from your cell itself!
A model is responsible to provide you with data, not the controller.
1. Have a model for displaying your collection view cell data
2. confirm them from your tableview cell itself!
extension yourTableViewCell: UICollectionViewDelegate {
// Respective methods
}
extension yourTableViewCell: UICollectionViewDataSource {
// Respective methods
}
Here is my implementation of tableView(_:cellForRowAt:):
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let index = indexPath.section
let weekDay = WeekDays.day(at: index)
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
return UITableViewCell()
}
Here is my code for my custom table view cell:
class NotSelectedCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = .red
self.textLabel?.numberOfLines = 0
self.textLabel?.textAlignment = .center;
self.textLabel?.text = "Not Available"
}
}
I've also tried initializing custom cell cell = NotSelectedCell() the result is the same. The content isn't shown. dataSource or viewDelegate aren't the problem as I'm working with UITableViewController.
Here's an image
The problem is awakeFromNIB "prepares the receiver for service after it has been loaded from an Interface Builder archive, or nib file." But you're instantiating this programmatically, so that method isn't called. You could theoretically move the code to init(style:reuseIdentifier:), make sure to call super in your implementation, and do any additional customization after that point.
But, you generally wouldn't programmatically instantiate cells when using static cells. (It's the point of static cells, that IB takes care of everything for you.) You generally don't implement UITableViewDataSource at all when using static cells.
I would advise using dynamic table and have two cell prototypes, one with reuse identifier of "NotAvailable" and one with "Available" (or whatever identifiers you want). Then programmatically instantiate the cell with the appropriate identifier. (By the way, this also has the virtue that your cell with "NotAvailable" can be designed entirely in IB, and no code is needed, for that cell at least.) This way, the storyboard takes care of instantiating the appropriate cell.
So, here I have two cell prototypes in my dynamic table, one for "not available" and one for "available":
Then the code would look at the model to figure out which to instantiate:
// for the complicated cell where I want to show details of some window of availability, add IBOutlets for that cell's labels
class AvailableCell: UITableViewCell {
#IBOutlet weak var startLabel: UILabel!
#IBOutlet weak var stopLabel: UILabel!
#IBOutlet weak var doctorLabel: UILabel!
}
// some super simple model to represent some window of availability with a particular doctor in that office
struct Availability {
let start: String
let stop: String
let doctor: String
}
class ViewController: UITableViewController {
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
let available = ...
override func numberOfSections(in tableView: UITableView) -> Int {
return days.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return available[days[section]]?.count ?? 1
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return days[section]
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// see if there are any available windows for the given day, if not, return "not available" cell
guard let availabilities = available[days[indexPath.section]] else {
return tableView.dequeueReusableCell(withIdentifier: "NotAvailable", for: indexPath)
}
// otherwise, proceed with the more complicated "Available" cell where I have to populate various labels and the like
let cell = tableView.dequeueReusableCell(withIdentifier: "Available", for: indexPath) as! AvailableCell
let availability = availabilities[indexPath.row]
cell.startLabel.text = availability.start
cell.stopLabel.text = availability.stop
cell.doctorLabel.text = availability.doctor
return cell
}
}
And that would yield:
Now, clearly, I just whipped up a super primitive model, and didn't do any UI design in the "available" cell prototype other than inserting three labels. But it illustrates the idea: If your dynamic table has multiple unique cell designs, just implement cell prototypes for each with unique identifiers and instantiate the appropriate one. And this way, you enjoy full cell reuse, minimize how much visual design you have to do programmatically, etc.
You are not supposed to use the cellForRow:atIndexPath method when using static cells. The cells are static, so the loading flow is different. What i'd suggest is to connect the cells individually from the interface builder to your view controller.
STILL, if you want to do it this way you have to get your cells by calling "super" since that's the class who is actually generating your static cells.
UITableView with static cells without cellForRowAtIndexPath. How to set clear background?
EDIT:
I just noticed that this is wrong:
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
You have to use the "dequeueReusable" method or something. Then again, these are STATIC Cells, so you should just be linking the cells directly from the interface builder.
Have a right view controller that slides in and out over the main view controller. This right view controller has a table in it to contain the passed information from the main.
I can access and pass the data to the controller from the main without issue but in the right view I need to then bind the data passed to it from the main.
The problem is that even though I try binding the data after the view comes into focus it gives nil on the tableView.reloadData().
RightViewController has 2 functions that are used by the main
func loadAlerts(){
self.tableView.reloadData()
}
func setAlerts(alerts: Alerts){
self.alerts = alerts
}
Alerts is just a custom object. It does contain values. self.alerts is a class variable.
MainViewController calls these 2 functions this way
self.rightViewController = self.storyboard?.instantiateViewController(withIdentifier: "RightViewController") as! RightViewController
Set the data after getting it from the api call
if let count = self.alerts?.Alerts.count {
if count == 0 {
return
}
//set on controller
rightViewController.setAlerts(alerts: self.alerts!)
}
This is defined at class level like
private var rightViewController: RightViewController!
Then I have a delegate defined for when the right controller is opened from a gesture and it calls like this
func rightDidOpen() {
rightViewController.loadAlerts()
}
This works fine for everything but the a tableView. Even by telling the tableView to load on the main thread like so
DispatchQueue.main.async{
self.tableView.reloadData()
}
Didn't change anything. At this point the alerts has values.
I don't mind refactoring the entire thing if need be so any ideas, thoughts or info of how I can get this to work is appreciated. If more info is needed just let me know.
--
Here the table delegate and source defined
class RightViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
and from front end assigned to the uicontroller (its calle Alerts Scene). Forgot to mention that if I do the api call directly in the right controller it works fine but I'm trying to reduce api calls so am refactoring this.
Here are the methods. Sorry for the misunderstanding.
//MARK: Tableview delegates
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let count = alerts?.Alerts.count{
return count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let alertD = alerts?.Alerts[indexPath.row] {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "AlertTableViewCell") as! AlertTableViewCell
cell.name.text = alertD.Summary
cell.icon.image = Helpers.listImage24dp(id: alertD.TOA)
cell.selectionStyle = .none
cell.name.textColor = UIColor.blue
return cell
}
return UITableViewCell()
}
There are two overloads for dequeueReusableCellWithIdentifier and I'm trying to determine when should I use one vs the other?
The apple docs regarding the forIndexPath function states, "This method uses the index path to perform additional configuration based on the cell’s position in the table view."
I'm not sure how to interpret that though?
The most important difference is that the forIndexPath: version asserts (crashes) if you didn't register a class or nib for the identifier. The older (non-forIndexPath:) version returns nil in that case.
You register a class for an identifier by sending registerClass:forCellReuseIdentifier: to the table view. You register a nib for an identifier by sending registerNib:forCellReuseIdentifier: to the table view.
If you create your table view and your cell prototypes in a storyboard, the storyboard loader takes care of registering the cell prototypes that you defined in the storyboard.
Session 200 - What's New in Cocoa Touch from WWDC 2012 discusses the (then-new) forIndexPath: version starting around 8m30s. It says that “you will always get an initialized cell” (without mentioning that it will crash if you didn't register a class or nib).
The video also says that “it will be the right size for that index path”. Presumably this means that it will set the cell's size before returning it, by looking at the table view's own width and calling your delegate's tableView:heightForRowAtIndexPath: method (if defined). This is why it needs the index path.
dequeueReusableCellWithIdentifier:forIndexPath: will always return a cell. It either re uses existing cells or creates a new one and returns if there are no cells.
While, the traditional dequeueReusableCellWithIdentifier: will return a cell if it exists i.e if there is a cell which can be reused it returns that else it returns nil. So you would have to write a condition to check for nil value as well.
To answer your question use dequeueReusableCellWithIdentifier: when you want to support iOS 5 and lower versions since dequeueReusableCellWithIdentifier:forIndexPath is only available on iOS 6+
Reference : https://developer.apple.com/library/ios/documentation/uikit/reference/UITableView_Class/Reference/Reference.html#//apple_ref/occ/instm/UITableView/dequeueReusableCellWithIdentifier:forIndexPath:
I have never understood why Apple created the newer method, dequeueReusableCellWithIdentifier:forIndexPath:. Their documentation on them is not complete, and is somewhat misleading. The only difference I've been able to discern between the two methods, is that that older method can return nil, if it doesn't find a cell with the identifier passed in, while the newer method crashes, if it can't return a cell. Both methods are guaranteed to return a cell, if you have set the identifier correctly, and make the cell in a storyboard. Both methods are also guaranteed to return a cell if you register a class or xib, and make your cell in code or a xib file.
For short:
dequeueReusableCell(withIdentifier, for) only works with prototype
cells. If you tried to use it when the prototype cell is absence, it would crash the app.
Hollemans M. 2016, Chapter 2 Checklist, IOS Apprentice (5th Edition). pp: 156.
The main difference is you can not register two cells for the same indexPath while only using the reuse identifier you can do it, and both can return nil if the cells are not registered against that table view
I would recommend to use both if you are using dynamic generated content. Otherwise your app might crash unexpectedly. You could implement your own function to retrieve an optional reusable cell. If it is nil you should return an empty cell that is not visible:
Swift 3
// Extensions to UITableView
extension UITableView
{
// returns nil, if identifier does not exist.
// Otherwise it returns a configured cell for the given index path
open func tryDequeueReusableCell (
withIdentifier identifier: String,
for indexPath: IndexPath) -> UITableViewCell?
{
let cell = self.dequeueReusableCell(withIdentifier: identifier)
if cell != nil {
return self.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
}
return nil
}
}
And the extension to return an empty cell:
// Extension to UITableViewCell
extension UITableViewCell
{
// Generates an empty table cell that is not visible
class func empty() -> UITableViewCell
{
let emptyCell = UITableViewCell(frame:CGRect(x:0, y:0, width:0, height:0))
emptyCell.backgroundColor = UIColor.clear
return emptyCell
}
}
A complete example of how to use it:
import Foundation
import UIKit
// A protocol is used to identify if we can configure
// a cell with CellData
protocol ConfigureAbleWithCellData
{
func configure(_ data: CellData)
}
class MyCustomTableViewCell :
UITableViewCell,
ConfigureAbleWithCellData
{
#IBOutlet weak var title:UILabel! = nil
func configure(_ data: CellData)
{
self.title.text = data.title
}
}
// This actually holds the data for one cell
struct CellData
{
var title:String = ""
var reusableId:String = ""
}
class CosmoConverterUnitTableViewController:
UIViewController,
UITableViewDelegate,
UITableViewDataSource
{
// Storage
var data = Array<Array<CellData>>()
func loadData()
{
var section1:[CellData] = []
var section2:[CellData] = []
section1.append(CellData(title:"Foo", reusableId:"cellType1"))
section2.append(CellData(title:"Bar", reusableId:"cellType2"))
data.append(section1)
data.append(section2)
}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return data[section].count
}
public func numberOfSections(in tableView: UITableView) -> Int
{
return data.count
}
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard
indexPath.row < data[indexPath.section].count
else
{
fatalError("this can't be")
}
let cellData = data[indexPath.section][indexPath.row]
if let cell = tableView.tryDequeueReusableCell(
withIdentifier: cellData.reusableId,
for: indexPath)
{
if let configurableCell = cell as? ConfigureAbleWithCellData
{
configurableCell.configure(cellData)
}
else
{
// cell is not of type ConfigureAbleWithCellData
// so we cant configure it.
}
return cell
}
// id does not exist
return UITableViewCell.empty()
}
}