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

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.

Related

How to access a referencing outlet from a different view controller?

I am new to Swift and Xcode. I am building an Financial Expense ios app.
In my first view controller, I created a referencing outlet for a label called expenseNum.
In my second view controller, I have a function for a button called Add Expense. When it is clicked, I need it to update the expenseNum variable with the amount of the expense.
What is the best way to go about this? I had created an object of the first view controller class and accessed it like "firstviewcontroller.expenseNum" but this will create a new instance of the class and I need it to be all the same instance so it can continuously add to the same variable. Thanks for the help!
You need a delegate
protocol SendManager {
func send(str:String)
}
In first
class FirstVc:UIViewcontroller , SendManager {
func send(str:string) {
self.expenseNum.text = str
}
}
when you present SecondVc
let sec = SecondVc()
sec.delegate = self
// present
In second
class SecondVc:UIViewcontroller {
var delegate:SendManager?
#IBAction func btnClicked(_ sender:UIButton) {
delegate?.send(str:"value")
}
}
// setting delegate
in viewDidLoad of SecondVc
if let first = self.tabBarController.viewControllers[0] as? FirstVc {
self.delegate = first
}
There are several ways you can pass data from ViewController2 to another ViewController1
The best way here is Protocol Delegates
Please follow below steps to pass data
In Your SecondViewController from where you want to send data back declare a protocol at the top of class declaration
protocol SendDataBack: class {
func sendDataFromSecondVCtoFirstVC(myValue: String)
}
Now in the class , declare a object of your protocol in same ViewController
weak var myDelegateObj: SendDataBack?
And now in your Add Expense button action just call the delegate method
myDelegateObj?.sendDataFromSecondVCtoFirstVC(myValue: yourValue)
Now go to your first ViewController
the place from where you have pushed/present to SecondViewController you must have taken the object of SecondVC to push to push from first
if let secondVC = (UIStoryboard.init(name: "Main", bundle: nil)).instantiateViewController(withIdentifier: "secondVCID") as? SecondViewController {
vc?.myDelegateObj = self
self.navigationController?.pushViewController(secondVC, animated: true)
**OR**
self.present(secondVC, animated: true, completion: nil)
}
now in your FirstViewController make an extension of FirstViewVC
extension FirstViewController: SendDataBack {
func sendDataFromSecondVCtoFirstVC(myValue: String) {
}
}
I think you can make a variable in your properties in second ViewController (before viewDidLoad method)
var delegate: FirstViewController? = nil
and use from the properties of the first view controller anywhere of the second view controller.
delegate!.mainTableView.alpha=1.0
//for example access to a tableView in first view controller
The simplest way to achieve this is to use a public var. Add a new Swift file to your project, call it Globals. Declare the public variable in Globals.swift like so:
public var theValue: Int = 0
Set its required value in the first ViewController, and you'll find you can read it in the second with ease.

(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.

Swift Delegation

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.

Storyboard UIView Objects Not Instantiating

I am working on a project with Swift and Storyboards. It's a conversion project from a traditional IB and Objective-C project. I am having an issue with a UITableView instantiating when the view is loaded. Let me explain.
The project is a navigation project. Here is an overview of the Storyboard.
The Storyboard's first viewController is HomeViewController and is a landing page that displays general info. The next VC is called FeedViewController shows a number of RSS feeds. You can see an expanded screen shot of the NavigationController, HomeViewController and FeedViewController in the picture below.
My problem is that I can't get the tableView to Instantiate. I first checked to make sure that my tableView was connected as an outlet and that the dataSource and delegate properties were connected. You can see this in the pic below.
In my FeedViewController class I have an Outler property called feedsTableView. You can see the declaration in the code below.
class FeedViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FLODataHandlerDelegate
{
// View Contoller and Protocol Properties
var floView : FLOViewController?
var dataHandler : FLODataHandler?
// Interface and Content Properties
var refreshControl : UIRefreshControl?
// IBOutlets
#IBOutlet weak var feedsTableView: UITableView!
#IBOutlet weak var backgroundImage: UIImageView!
In the HomeViewController I have a FeedViewController property that I intend to use to gain access to FeedViewController's feedsTableView.
class HomeViewController: UIViewController, FLODataHandlerDelegate, MFMailComposeViewControllerDelegate
{
// View Contoller and Protocol Properties
var feedViewController : FeedViewController?
var dataHandler : FLODataHandler?
When HomeViewController's viewDidLoad() method is called I start the dataHandler - which instantiates the FeedViewController - and set it to my FeedViewController property.
override func viewDidLoad()
{
super.viewDidLoad()
// Set up the gesture recognizer to allow for swiping to the feed VC.
let recognizer = UISwipeGestureRecognizer(target: self, action: Selector("goToNext"))
recognizer.direction = UISwipeGestureRecognizerDirection.Left
self.view.addGestureRecognizer(recognizer)
// Start the data handler
self.setUpDataHandler()
}
setUpDataHandler()
func setUpDataHandler()
{
// Intitalize FeedVC for use later in the VC
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("FeedViewController") as! FeedViewController
self.feedViewController = vc
}
I also have a fail safe that if someone were to go to the FeedViewController before the setUpDataHandler() method is called then I instantiate FeedViewController here as well.
func goToNext()
{
// Grab the feedViewController so it can be pushed onto the stack. Make sure you set up the storyboard identifier.
let feedVC = self.storyboard!.instantiateViewControllerWithIdentifier("FeedViewController") as! FeedViewController
self.feedViewController = feedVC
self.navigationController!.pushViewController(self.feedViewController!, animated: true)
}
However the feedsTableView is not getting instantiated. In the viewDidLoad() method of FeedViewController I attempt to add the feedsTableView to a UIRefreshController.
override func viewDidLoad()
{
super.viewDidLoad()
self.refreshControl = UIRefreshControl()
self.refreshControl!.addTarget(self, action: "refreshInvoked:state:", forControlEvents: UIControlEvents.ValueChanged)
// See the note in viewDidLoad in FLOViewController.
self.feedsTableView.addSubview(self.refreshControl!)
}
When the app runs I get the following error.
fatal error: unexpectedly found nil while unwrapping an Optional value
The image below shows were this is called. It's the viewDidLoad() of the FeedViewController. As you can see in the picture I even tried instantiating the feedsTableView before adding it to the UIRefreshController and I still get the error.
Any help would be greatly appreciated.
Take care,
Jon
The reason why it doesn't work in the very last case, where you manually instantiate UITableView and assign that to self.feedsTableView, is that self.feedsTableView is declared weak. Thus, the table view comes into existence, is assigned, and vanishes in a puff of smoke because it has no memory management. By the time you get to the last line, self.feedsTableView is nil once again.
Thus, the solution for that last case is to remove the weak designation from your feedsTableView declaration.
That will get you past the crash in that last case. But of course you won't see anything because you are not also inserting the table view into your interface.