Swift Delegation - swift

I'm having trouble wrapping my head around delegation in Swift. After reading some guides, I was able to set it up delegation between two ViewControllers, but I'm not understanding how it works. In my first view controller, I have a a label that displays what has been entered in the second view controller which contains a text field and a button (that returns to the first view controller). Here is the code for the first view controller:
#IBOutlet weak var labelText: UILabel!
func userDidEnterInformation(info: String) {
labelText.text = info;
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "transition"){
let secondVC: SecondViewController = segue.destinationViewController as! SecondViewController;
secondVC.delegate = self;
}
}
Here's the code for the second view controller:
protocol DataEnteredDelegate{
func userDidEnterInformation(info: String);
}
#IBOutlet weak var userText: UITextField!
var delegate: DataEnteredDelegate? = nil;
#IBAction func buttonPressed(sender: AnyObject) {
let information = userText.text!;
delegate!.userDidEnterInformation(information);
self.navigationController?.popToRootViewControllerAnimated(true);
}
My understanding is that in the text inside the text field gets stored in the information constant, then the userDidEnterInformation method from the protocol is called, with the method being defined inside the first view controller. This method then changes the label inside the first view controller. The thing is, I'm not sure what is happening in the prepareForSegue function. Specifically, I'm not sure what's the purpose of secondVC.delegate = self.
I would appreciate any sort of clarity on delegation.

The diagram is simple but can help you understand what's going on.
FirstViewController must conform to the DataEnteredDelegate protocol you have defined (see Sumit's answer). When using secondVC.delegate = self, you are saying that for the segue transition with the destination being a SecondViewController, the attribute delegate of that SecondViewController instance will be set to this instance of FirstViewController, thus delegating things from SecondViewController to your FirstViewController as made possible by the DataEnteredDelegate protocol.

The protocol you created in second viewcontroller is an Interface. You must implement your first view controller with the DataEnteredDelegate protocol.
class FirstViewController:UIViewController, DataEnteredDelegate{
func userDidEnterInformation(info: String) {
//stub
}
}

If the delegate of the second VC is not set in prepareForSegue() it remains nil. The second VC is then unable to call the first VC.
On a side note, if the delegate is nil your code will crash because delegate! is trying to unwrap an optional binding with the value of nil. It's better to first unwrap the delegate variable:
if let handler = delegate {
handler.userDidEnterInformation(information)
}
Alternatively, you could use Swift's Optional Chaining, calling userDidEnterInformation only if delegate is not nil.
delegate?.userDidEnterInformation(information);
In addition it is recommended to declare the delegate weak, to prevent retain cycles:
weak var delegate: DataEnteredDelegate?

Delegates and Protocols
Do not try to figure out how the dictionary definition of “delegate” fits with the concept of delegation in Swift. It doesn't.
Delegation in Swift is an agreement between two players—a sensing object and a requesting object. The “delegate” is the “requesting object.” Just think “asker” or “requester” every time you see “delegate” and it will make a lot more sense. Here is their agreement...
The Sensing Object (Second View Controller):
I have data from some event that took place. I will publish instructions (a protocol) on how you may access that data. If you want it, you must do three things.
You must declare in your class type that your class abides by my protocol.
You must write the functions that I describe in my protocol. I don't care what those functions do but the function type must match what I publish.
In YOUR code, you must set MY “delegate” (think “asker”) property to point to you. {secondVC.delegate = self} That way I can call one of YOUR functions to deliver the data.
After that, when I get some data, I will call one of the functions in your object that I told you to write. My call to your function will contain the data you are looking for as one of the arguments. {delegate!.userDidEnterInformation(information)} Note: delegate! (asker!) is YOU.
The Delegate (Requesting) Object (First View Controller):
O.K. You've got a deal.

Related

Swift: Reload data in UITableView element triggered in an external class

I'm trying to call the reloadData() method to update a table view in an external class (different view controller), using the method viewDidDisappear().
I can already update the table view when the view where it is located is loaded or appears:
class OrderHistoryController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var orderTable: UITableView!
//called when order table is loaded or appears
override func viewDidLoad() {
super.viewDidLoad()
self.orderTable.delegate = self
self.orderTable.dataSource = self
self.orderTable.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
self.orderTable.delegate = self
self.orderTable.dataSource = self
self.orderTable.reloadData()
}
// ...
}
But I want the table to be reloaded when another view disappears.
class OrderDocumentationController: UIViewController {
override func viewDidDisappear(_ animated: Bool) {
OrderHistoryController().orderTable.reloadData()
return
}
// ...
}
I get the fatal error:
Unexpectedly found nil while implicitly unwrapping an Optional value.
Guess it is just a rookie mistake. Any ideas? Thank you in advance!
When you call OrderHistoryController().orderTable.reloadData(), this will create a new OrderHistoryController instance, that will have not orderTable outlet connected hence resulting in the crash.
There are differnt ways to achieve what you want:
Store a reference to the OrderHistoryController instance and use this
Maybe better: Implement some custom delegation mechanism
Use the NotificationCenter to send a message that then will result in a refresh
No need to reload orderTable if OrderHistoryController appears when OrderDocumentationController disappears. Since, self.orderTable.reloadData() is called in OrderHistoryController::viewDidAppear()
UPDATE:
A better approach would be to have OrderDocumentationController provide a block which is called when the modal controller has completed.
So in OrderDocumentationController, provide a block property say with the name onDoneBlock.
In OrderHistoryController you present as follows:
In OrderHistoryController, create OrderDocumentationController
Set the done handler for OrderDocumentationController as: OrderDocumentationController.onDoneBlock={[OrderDocumentationController dismissViewControllerAnimated:YES completion:nil]};
Present the OrderDocumentationController controller as normal using [self OrderDocumentationController animated:YES completion:nil];
In OrderDocumentationController, in the cancel target action call self.onDoneBlock();
The result is OrderDocumentationController tells whoever raises it that it is done. You can extend the onDoneBlock to have arguments which indicate if the modal completed, cancelled, succeeded etc.

How to instantiate a weak delegate without triggering "Instance will be immediately deallocated because property 'tableViewDelegate' is 'weak'"

I am trying to separate out my tableView's datasource into a separate delegate object. Since that delegate needs to access the tableview at some point I need a reference to the delegating object in the delegate; and since both are classes I need to avoid strong reference cycles by making the delegate weak
To achieve this I tried the following code.
class MyViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
weak var tableViewDelegate: UITableViewDataSource?
override func viewDidLoad() {
super.viewDidLoad()
tableViewDelegate = TableViewDelegate() // throwing a warning
tableView.dataSource = tableViewDelegate
}
}
When I try to instantiate the delegate Xcode throws a warning: "Instance will be immediately deallocated because property 'tableViewDelegate' is 'weak'"
So to fix it I do the following:
class MyViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
weak var tableViewDelegate: UITableViewDataSource?
override func viewDidLoad() {
super.viewDidLoad()
let delegate = TableViewDelegate() // worried this creates a strong reference.
self.tableViewDelegate = delegate
tableView.dataSource = delegate
}
}
Please confirm if the following is true or not: by initialising the delegate in the viewDidLoad() method I am not in danger of creating a strong reference because the variable that holds that instance is deallocated as soon as we leave the scope of that method. Or to put it another way: the only time we need to worry about a variable (which points to a class) creating a strong reference is if the variable is initialised at the class level, and hence will live on as long as the class does.
Is that correct?
Please confirm if the following is true or not: by initialising the delegate in the viewDidLoad() method I am not in danger of creating a strong reference because the variable that holds that instance is deallocated as soon as we leave the scope of that method.
Correct. The strong reference goes away as soon as the scope in which the let is declared exits.
Unfortunately, that means your delegate will still be deallocated. All you have done is silence the warning.
Basically, you need to have a strong reference to the delegate somewhere or it will go away straight away. My feelings is that you should make the reference in MyViewController strong. There will be no strong reference cycle as long as your delegate does not contain a strong reference to the view controller. If you need a reference to MyViewController in the delegate, make that a weak one i.e. the view controller owns the delegate, not the delegate owns the view controller.
Response to the comment below:
almost all the tutorials I have found have the delegate property as weak, so it seems standard practice.
Yes it is fairly standard practice, there are exceptions including in Cocoa. However, it is standard practice to have a weak reference to the delegate in the delegating object. In your case, the delegating object is thew UITableView not the MyViewController. In your first example from the Internet, the FileImporter is analogous to the UITableView in your code. In the second example, the DetailViewController is the delegating object.
If you think about it, your TableViewDelegate is being used in place of making MyViewController conform to the protocol. It makes absolute sense that the MyViewController would own the delegate.
This is how I solved this problem:
let dataSource = MyDataSource()
lazy var viewModel : MyViewModel = {
let viewModel = MyViewModel(dataSource: dataSource)
return viewModel
}()
and then in viewDidLoad():
tableView.delegate = self
tableView.dataSource = dataSource
You can see the full demo project here

Are Swift segues closely coupled?

In Swift using MVC a common way to send data forward is to use prepare(for:sender:). Inside that method you get a reference to the destination VC and access its properties to send the data. But isn't that considered coupling the view controllers? I'm hoping the answer isn't considered a matter of opinion because I'd really like to understand how segues fit into MVC.
Passing data in a segue is completely compatible with MVC. The destinationVC is considered as a View of the sourceVC. When a controller communicates with a View, it configures the View with the data it needs. Writing to the public interface (properties) of the destinationVC is how you set it up. This is what is happening in prepare(for segue:sender).
The concern about coupling relates to reuse. The more tightly coupled the viewControllers are, the harder it is to reuse them. This is only a problem if the destinationVC knows details of the sourceVC. If the destinationVC needs to pass data back to the sourceVC, it should do so using delegation (where a protocol is used to define the methods that the sourceVC implements).
If view controller A segues to view controller B and assumes it is of type ViewControllerB then yes, that is tight coupling:
prepare(for: segue, sender: sender) {
if let viewControllerB = segue.destination as? ViewControllerB {
viewControllerB.property = value
}
}
That code only works if the destination is a specific class, ViewControllerB, and the first view controller has to know the properties of ViewControllerB.
That's not usually a problem because usually when you segue to another view controller you know what you've asked for, and hence what to expect.
However, you might use the same prepare(for:sender:) method to segue to any of several different types of view controllers that have common properties.
In that case you can use protocols to make the coupling looser:
Approach 2: Loosely coupled using a protocol
protocol DestProtocol {
var property: String
}
You might have a ViewControllerB that conforms to DestProtocol
class ViewControllerB: UIViewController, DestProtocol {
var property: string
//The rest of ViewControllerB
}
And you might also have a ViewControllerC that conforms to DestProtocol
class ViewControllerC: UIViewController, DestProtocol {
var property: string
//The rest of ViewControllerC
}
Then in the first view controller's prepare(for:sender:):
prepare(for: segue, sender: sender) {
if let destination = segue.destination as? DestProtocol {
destination.property = value
}
}
With the second approach the first view controller's prepare(for:sender) doesn't know that the destination is an instance of ViewControllerB. It just checks to see if the destination conforms to the DestProtocol. The segue could be loading an instance of ViewControllerB or ViewControllerC.

(re)-Pass data after click on backbutton

I'm trying to pass data from a SecondViewController to my FirstViewController when I click on my back button (UINaviagtionController).
For pass my data from FirstViewController to the SecondViewController I do this:
if segue.identifier == "showSecondVC" {
let vc = segue.destination as! SecondViewController
vc.rows = rows[pathForAVC]
vc.lap = lapTime[pathForAVC]
vc.indexPath = pathForAVC
}
But I have no idea how to pass data from SecondViewController to the FirstViewController and I really don't understand topics about it on Stack Overflow.
I want to transfer it when I click here:
Thanks.
You can use delegate pattern for that. You can grab the back button press event like this and update the data
override func viewWillDisappear(_ animated: Bool) {
if self.isMovingFromParentViewController {
self.delegate.updateData( data)
}
}
For more information on delegates you can go through this.
Actually things depend on your requirement, if you want data to be updated in first view controller as soon as it is updated in second view controller, you would need to call delegate as soon as the data is updated. But as in the question you have mentioned that you want it to be updated on back button only, above is the place to do it.
Another way would be to have Datasource as singleton so that it is available to all the view controllers and the changes are reflected in all view controllers. But create singleton if absolutely necessary, because these nasty guys hang around for entire time your application is running.
You should have a custom protocol such as:
public protocol SendDataDelegate: class {
func sendData(_ dataArray:[String])
}
Here I suppose you want to send a single array back to FirstViewController
Then make your first view controller to conform to the custom protocol, such as:
class FirstViewController: UIViewController, SendDataDelegate
In the second view controller, create a delegate a variable for that protocol, such as:
weak var delegate: SendDataDelegate?
and then you catch the back action and inside it you call your custom protocol function, such as:
self.delegate?.sendData(arrayToSend)
In the first viewController, in the prepare for segue function just set the delegate like
vc.delegate = self

Change #IBOutlet from a subview

I'm trying to enable or disable an #IBOutlet UIButton Item of a toolbar from a UIView.
The button should get disabled when the array that I'm using in EraseView.Swift is empty.
I tried creating an instance of the view controller but it gives me the error (found nil while unwrapping):
in EraseView:
class EraseView: UIView {
...
let editViewController = EditImageViewController()
//array has item
editViewController.undoEraseButton.enabled = true //here I get the error
...
}
I tried to put a global Bool that changed the value using it in EditImageViewController but it doesn't work:
var enableUndoButton = false
class EditImageViewController: UIViewController {
#IBOutlet weak var undoEraseButton: UIBarButtonItem!
viewDidLoad() {
undoEraseButton.enabled = enableUndoButton
}
}
class EraseView: UIView {
...
//array has item
enableUndoButton = true //here I get the error
...
}
I know it's simple but I can't let it work. Here's the situation:
The root of the problem is the line that says:
let editViewController = EditImageViewController()
The EditImageViewController() says "ignore what the storyboard has already instantiated for me, but rather instantiate another view controller with no outlets hooked up and use that." Clearly, that's not what you want.
You need to provide some way for the EraseView to inform the existing view controller whether there was some change to its "is empty" state. And, ideally, you want to do this in a way that keeps these two classes loosely coupled. The EraseView should only be informing the view controller of the change of the "is empty" state, and the view controller should initiate the updating of the other subviews (i.e. the button). A view really shouldn't be updating another view's outlets.
There are two ways you might do that:
Closure:
You can give the EraseView a optional closure that it will call when it toggles from "empty" and "not empty":
var emptyStateChanged: ((Bool) -> ())?
Then it can call this when the state changes. E.g., when you delete the last item in the view, the EraseView can call that closure:
emptyStateChanged?(true)
Finally, for that to actually do anything, the view controller should supply the actual closure to enable and disable the button upon the state change:
override func viewDidLoad() {
super.viewDidLoad()
eraseView.emptyStateChanged = { [unowned self] isEmpty in
self.undoEraseButton.enabled = !isEmpty
}
}
Note, I used unowned to avoid strong reference cycle.
Delegate-protocol pattern:
So you might define a protocol to do that:
protocol EraseViewDelegate : class {
func eraseViewIsEmpty(empty: Bool)
}
Then give the EraseView a delegate property:
weak var delegate: EraseViewDelegate?
Note, that's weak to avoid strong reference cycles. (And that's also why I defined the protocol to be a class protocol, so that I could make it weak here.)
The EraseView would then call this delegate when the the view's "is empty" status changes. For example, when it becomes empty, it would inform its delegate accordingly:
delegate?.eraseViewIsEmpty(true)
Then, again, for this all to work, the view controller should (a) declare that is conforms to the protocol; (b) specify itself as the delegate of the EraseView; and (c) implement the eraseViewIsEmpty method, e.g.:
class EditImageViewController: UIViewController, EraseViewDelegate {
#IBOutlet weak var undoEraseButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
eraseView.delegate = self
}
func eraseViewIsEmpty(empty: Bool) {
undoEraseButton.enabled = !empty
}
}
Both of these patterns keep the two classes loosely coupled, but allow the EraseView to inform its view controller of some event. It also eliminates the need for any global.
There are other approaches that could solve this problem, too, (e.g. notifications, KVN, etc.) but hopefully this illustrates the basic idea. Views should inform their view controller of any key events, and the view controller should take care of the updating of the other views.