How to communicate between Classes in a hierarchy in Swift - ios

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!

Related

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
}
}

Subclass UIButton to save Data?

I was wondering if there is any problem with extending UIButton in order to have a ref of my Data?
For instance I have a table view and whenever user clicks on a cell I need to provide the user with the data for that specific cell.
Data is kept inside an array and array index was saved in UIButton tag but as my array gets updated, wrong indexes will be provided. So what i was trying to do was to extend a uibutton and then have a variable which holds my model.
This idea works fine but as Im not really experienced in swift I wanted to know what are the drawbacks and problems of doing such a thing.
You don't need to save the index as Button's tag. Subclassing the UIButton as Sneak pointed out in comment is clearly a very bad idea. On top of that saving your model in a UIComponent is disasters design.
You can do it multiple ways. One that I find neat :
Step 1:
Create a Class for your Custom Cell. Lets say MyCollectionViewCell. Because your Cell has a button inside it, you should create IBAction of button inside the Cell.
class MyCollectionViewCell: UICollectionViewCell {
#IBAction func myButtonTapped(_ sender: Any) {
}
}
Step 2:
Lets declare a protocol that we will use to communicate with tableView/CollectionView's dataSource.
protocol MyCollectionViewCellProtocol : NSObjectProtocol {
func buttonTapped(for cell : MyCollectionViewCell)
}
Step 3:
Lets create a property in our MyCollectionViewCell
weak var delegate : MyCollectionViewCellProtocol? = nil
After step 3 your MyCollectionViewCell class should look like
protocol MyCollectionViewCellProtocol : NSObjectProtocol {
func buttonTapped(for cell : MyCollectionViewCell)
}
class MyCollectionViewCell: UICollectionViewCell {
weak var delegate : MyCollectionViewCellProtocol? = nil
#IBAction func myButtonTapped(_ sender: Any) {
self.delegate?.buttonTapped(for: self)
}
}
Step 4:
In your tableView's CellForRowAtIndexPath or CollectionView's sizeForItemAtIndexPath confirm pass ViewController as delegate to cell.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : MyCollectionViewCell = //deque your cell
(cell as! MyCollectionViewCell).delegate = self
}
Step 5:
Now make your ViewController confirm to protocol
class YourViewController: UIViewController,MyCollectionViewCellProtocol {
Step 6:
Finally implement method in your VC
func buttonTapped(for cell: MyCollectionViewCell) {
let indexPath = self.collectionView?.indexPath(for: cell)
//access array now
}
P.S:
Sorry though I know you are using TableView, in a hurry I have written code for CollectionView but the delegates are pretty same :) I hope you will be able to understand the logic

Swift: How to access a mutable array of strings from one UIViewController to a TableView cell file

I have one view controller named TableViewController and another customised cell called feed.swift
The cells are getting reused properly and I have put tags on various buttons as I wan't to know what button of what feed is pressed on.
In my cellForRowAtIndexPath I'm populating my username with json that I have parsed. It looks like this
cell.username.text = username[indexPath.row]
output-> ["andre gomes", "renato sanchez", "renato sanchez"]
Then I have tagged my username button like this
cell.usernamePress.tag = indexPath.row
This is going on in my TableViewController
In my feed.swift I'm checking if a button is pressed and printing out the tag assigned to that button
#IBAction func usernameBut(sender: AnyObject) {
print(usernamePress.tag)
}
output-> 2
Now I need to access the username array of TableViewController in feed.swift and do something like username[usernamePress.tag]
I tried making a global.swift file but I'm not able to configure it for an array of strings.
import Foundation
class Main {
var name:String
init(name:String) {
self.name = name
}
}
var mainInstance = Main(name: "hello")
Even after doing this I tried printing mainInstance.name and it returned hello even after changing it. I want a solution where the array of strings holds the values I set in TableViewController and I can be able to use them in feed.swift
Any suggestions would be welcome! I'm sorry if there are any similar question regarding this but I'm not able to figure out how to use it for a mutable array of strings
I suggest you don't use the array directly in your FeedCell but instead return the press-event back to your TableViewController where you handle the event. According to the MVC Scheme, which is the one Apple requests you to use (checkout Apples documentation), all your data-manipulation should happen in the Controller, which then prepares the Views using this data. It is not the View that is in charge to display the right values.
To solve your problem I would choose to pass back the press-event via the delegation-pattern, e.g. you create a FeedCellDelegate protocol that defines a function to be called when the button is pressed:
protocol FeedCellDelegate {
func feedCell(didPressButton button: UIButton, inCell cell: FeedCell)
}
Inside your FeedCell you then add a delegate property, which is informed about the event by the View:
class FeedCell {
var delegate: FeedCellDelegate?
...
#IBAction func pressedUsernameButton(sender: UIButton) {
delegate?.feedCell(didPressButton: sender, inCell: self)
}
}
If your TableViewController then conforms to the just defined protocol (implements the method defined in there) and you assign the ViewController as the View's delegate, you can handle the logic in the Controller:
class TableViewController: UITableViewController, FeedCellDelegate {
...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FeedCell", forIndexPath: indexPath) as! FeedCell
cell.delegate = self
// Further setup
return cell
}
func feedCell(didPressButton button: UIButton, inCell cell: FeedCell) {
guard let indexPath = tableView.indexPathForCell(cell) else { return }
// Do your event-handling
switch (button.tag) {
case 2: print("Is username button")
default: print("Press not handled")
}
}
}
As you might recognize I changed your class name. A Feed sounds more like a Model-class whereas FeedCell implies its role to display data. It makes a programmer's life way easier if you choose self-explaining names for your classes and variables, so feel free to adapt that. :)
you should add a weak array property to the tableViewCell:
weak var userNameArray:[String]?
Then in your tableViewController pass the username array into the cell:
fun tableView(tableView:UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// create the cell, then...
if let array = self.username {
cell.userNameArray = array
}
}
Then you can use the array in the cell itself to populate its fields handle button taps, etc.

Swift: How can I bind an NSIndexPath to a addTarget

I have a UISwitch in each of my dynamically created rows of which I want to bind an NSIndexPath to an addTarget selector.
I had considered allowing the user to tap the row to toggle the switch, but from a UX perspective it makes more sense to have the switch handle this method, therefore using didSelectRowAtIndexPath is not an appropriate solution.
When my cell is created I currently bind a selector like this:
// Create listener for each switch
prefCell.subscribed?.addTarget(self,
action: "switchFlipped:",
forControlEvents: UIControlEvents.ValueChanged)
Which calls the corresponding method:
func switchFlipped(flipSwitch: UISwitch, indexPath: NSIndexPath) {}
Obviously this throws an error because NSIndexPath isn't a valid reference, as I believe it will only send the buttons reference. Is there any way I can bind the index path? If not, is there any way to get the current cell from the context of the UISwitch?
The target-action pattern does not allow arbitrary selectors. You can have f(sender:) and f(sender:event:). Both won't help you. But you can use code to figure out the indexPath in the function.
Code should be self explanatory:
func switchFlipped(flipSwitch: UISwitch) {
let switchOriginInTableView = flipSwitch.convertPoint(CGPointZero, toView: tableView)
if let indexPath = tableView.indexPathForRowAtPoint(switchOriginInTableView) {
println("flipped switched at \(indexPath)")
}
else {
// this should not happen
}
}
This should be the a good solution, not depending on a point. You call the superviews of the Switch and therefore calculate the index path of the cell.
func switchTapped(sender: UISwitch) -> NSIndexPath {
let cell = sender.superview!.superview! as UITableViewCell
return tableView.indexPathForCell(cell)!
}
This reliably works, that's why you can simply unwrap any optionals.
You can try 3 approaches, as you see fit.
Create a custom UISwitch class and add an NSIndexPath property to it. When you receive notification, type cast to your custom class and access the NSIndexPath.
Create a custom UITableViewCell class and save NSIndexPath to it. When you get notified of UISwitch, get its superviews and see which one is your customUITableViewCell instance and get the property.
If you only have one section, it means all you need to worry about are rows. Set the UISwitch tag as the row number and access it when switch is flipped.
The Most Tech-Savy Solution Use Extensions of Swift.
Extensions add new functionality to an existing class, structure, or enumeration type. This includes the ability to extend types for which you do not have access to the original source code
Apple Docs
Use delegation.
Your switch's target action should be a method in your custom UITableViewCell. That custom tableviewcell should declare a protocol and the tableview should be it's delegate. Then your tableviewcell should call its delegate method from within the switch's target function.
The delegate method for your tableviewcell should have a parameter in which the cell passes self. Then in your tableviewcontroller delegate implementation you can use indexPathForCell:
UPDATE:
Here is the apple doc for swift protocols and delegates:
https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html
Simply put, you need to define a protocol
protocol CustomUITableViewCellDelegate {
func tableViewCellSubscribed(cell: myCustomUITableViewCell) -> Void
}
then you make your tableviewcontroller class conform to that protocol like this:
class myTableViewController: UITableViewController, CustomUITableViewCellDelegate {
func tableViewCellSubscribed(cell: myCustomUITableViewCell) -> Void {
//this is where you handle whatever operations you want to do regarding the switch being valuechanged
}
// class definition goes here
}
Then your custom UITableViewCell class should have a delegate property like this:
class myCustomUITableViewCell : UITableViewCell {
var delegate : CustomUITableViewCellDelegate?
//class definition goes here
}
Finally, set the cell's delegate in tableView:cellForRowAtIndexPath: and you're good to go. You just need to call your tableViewCellSubscribed: delegate function from within the target method of your switch action.
I've used this approach in many projects, you can just add a callback to your custom cell, each time your switch changes, you can run this callback and catch it inside your table datasource implementation, below a dirt and simple example.
1. Create a UITableViewCell custom class
// An example of custom UITableViewCell class
class CustomCell:UITableViewCell {
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var onSwitchChange:() -> Void?
}
class TableDataSource:UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:CustomCell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as CustomCell
cell.onSwitchChange = {
NSLog("Table: \(tableView) Cell:\(cell) Index Path: \(indexPath)")
}
}
}
Acctually,Apple has already provide API to get the indexPath.
Simply like this (in Objective-C)
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
Far easier, you can use the tag variable for this :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.mySwitch.tag = indexPath.item
cell.mySwitch.addTarget(self, action: #selector(switchChanged), for: UIControlEvents.valueChanged)
...
}
func switchChanged(mySwitch: UISwitch) {
let indexPathItem = mySwitch.tag
...
}
From official doc :
var tag: Int { get set }
An integer that you can use to identify view objects in your
application.
The default value is 0. You can set the value of this tag
and use that value to identify the view later.

How to set datasource and delegate Outside of View Controller

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.

Resources