Reload and not reload if press back from different view controllers. Swift - ios

The top three answers can solve my questions. It is hard to pick which one is the best. So, I just pick the one who is the first to answer my question. Sorry for amateur and iOSEnthusiatic. Thank you for your help. I appreciate it.
ViewController 1 has a table view.
My question is how to reload the table view only if I click back from view controller 2, and not reload the table view if I click back from view controller 3.
Right now, my code for back button is
#IBAction func backButtonTapped(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
In view controller 1. I know that the table view would be reloaded from either view controller 2 or 3
override func viewDidAppear(animated: Bool) {
loadTable()
}
I tried to put loadTable() in viewDidLoad and try to write the below code for back button in view controller 2. But, it doesn't work.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewControllerWithIdentifier("UserHomePageViewController") as! UserHomePageViewController
controller.viewDidLoad()
Any suggestion what I should do? Thank you for your help.
EDIT:
I think this is an easier way to do it, but it still does not work as I thought. I guess it is because the viewDidAppear is executed before the call of reloadTableBool. Correct? Is there any way to fix it? Thank you. You help would be appreciated.
class 2ViewController
#IBAction func backButtonTapped(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewControllerWithIdentifier("1ViewController") as! 1ViewController
print("viewcontroller2 before call: \(controller.reloadTableBool)")
controller.reloadTableBool = false
print("viewcontroller2 after call: \(controller.reloadTableBool)")
self.dismissViewControllerAnimated(true, completion: nil)
}
class 1ViewController
var reloadTableBool = true
override func viewDidAppear(animated: Bool) {
print("viewcontroller1: \(reloadTableBool)")
if reloadTableBool == true {
loadTable()
}
}
When I click back on view controller 2, it prints
viewcontroller2 before call: true
viewcontroller2 after call: false
viewcontroller1: true

Here is a link to a question I answered a couple days ago. Use the navigation controller delegate to handle the back button. In your second view controller, set the delegate to self and reload the tableview when you press the back button.
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if let controller = viewController as? FirstViewController {
controller.tableView.reloadData()
}
}
NOTE:
I'm assuming you're using the back button of the navigation controller here.
EDIT: Another example using your manually added back button:
#IBAction func backButtonTapped(sender: AnyObject) {
if let viewControllers = app.window?.rootViewController?.childViewControllers {
viewControllers.forEach { ($0 as? FirstViewController)?.tableView.reloadData() }
}
self.dismissViewControllerAnimated(true, completion: nil)
}
Seeing as you are using a navigation controller:
#IBAction func backButtonTapped(sender: AnyObject) {
navigationController?.viewControllers.forEach { ($0 as? FirstViewController)?.tableView.reloadData() }
self.dismissViewControllerAnimated(true, completion: nil)
}

If displaying vc2 is performed by vc1 and is always sure to invalidate the data in vc1, you could do the following:
add a needsReload boolean instance variable to vc1
set it to true whenever you display vc2 (and when instanciating vc1 eg in awakeFromNib if coming from a storyboard)
only perform the content of loadTable if needsReload is true (maybe refactor this logic into a loadTableIfNeeded)
don't forget to set needsReload to false in the end of loadTableIfNeeded
This invalidation pattern is found throughout UIKit, see for example UIView setNeedsLayout/layoutIfNeeded. The advantage is that even if several events cause the data to invalidate, it will only actually get refreshed when you need it.
In your situation it has the additional advantage of keeping the logic contained in vc1 and not creating unnecessary coupling between your VCs, which is always good.
---UPDATE: sample implementation (ObjC but you'll get the idea)
You only need to handle this in VC1, forget about all the back button stuff in VC2. This implementation will mark VC1 for reload as soon as VC2 is presented, but will actually reload only on viewWillAppear, when VC2 is dismissed.
---UPDATE 2: Added a conditional reload based on a delegate callback
Note that _needsReload is now set in the delegate callback, not when VC2 is first presented. Instead we set VC1 as the delegate of VC2. (_needsReload logic is actually unnecessary using this method, kept it for reference)
//VC2: add a delegate to the interface
#class VC2;
#protocol VC2Delegate
- (void) viewController:(VC2*)myVC2 didFinishEditingWithChanges:(BOOL)hasChanges;
#end
#interface VC2
#property (nonatomic, weak) id<VC2Delegate> delegate
#end
#implementation VC2
- (IBAction) finishWithChanges
{
[self.delegate viewController:self didFinishEditingWithChanges:YES];
}
- (IBAction) finishWithoutChanges
{
[self.delegate viewController:self didFinishEditingWithChanges:NO];
}
#end
//VC1: implement the VC2Delegate protocol
#interface VC1 () <VC2Delegate>
#end
#implementation VC1
{
BOOL _needsReload
}
- (void) awakeFromNib
{
//adding this for completeness but the way you did it in Swift (at init) is correct
[super awakeFromNib];
_needsReload = YES;
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self reloadTableIfNeeded];
}
- (IBAction) displayVC2
{
VC2* myVC2 = ... //instanciate your VC2 here
myVC2.delegate = self; //set as the delegate of VC2
[self presentViewController:myVC2 animated:YES completion:nil];
}
- (void) viewController:(VC2*)myVC2 didFinishEditingWithChanges:(BOOL)hasChanges
{
_needsReload = hasChanges;
[self reloadTableIfNeeded];
}
- (void) reloadTableIfNeeded
{
if (_needsReload) {
[self.tableView reloadData];
_needsReload = NO;
}
}
#end

You can use notification approach easily for this.
Add observer in your 1st ViewController in viewDidLoad method.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadTable:", name: "reloadTable", object: nil)
func reloadTable(notification : NSNotification){
let isReload : NSNumber = notification.userInfo!["isReload"] as! NSNumber
if (isReload.boolValue) {
self.tableView.reloadData()
}
}
Then post notification like this from your 2nd and 3rd ViewController respectively when you call dismissViewController.
// From 2nd viewcontroller
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil, userInfo: ["isReload" : NSNumber(bool: false)])
// From 3rd viewcontroller
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil, userInfo: ["isReload" : NSNumber(bool: true)])

Related

How to popViewController and send back data

I have the following code to go back to the last view controller
self.navigationController?.popViewController(animated: true)
How do I send data back to the last view controller as I do this?
Swift relies a lot on the delegate pattern and this is a good place to use it.
class FirstViewController: UIViewController {
func pushToSecondViewController() {
let second = SecondViewController()
second.firstViewControllerDelegate = self // set value of delegate
navigationController?.pushViewController(second, animated: true)
}
func someDelegateMethod() {
print("great success")
}
}
class SecondViewController: UIViewController {
weak var firstViewControllerDelegate: FirstViewController? // establish a delegate
func goBackToFirstViewController() {
firstViewControllerDelegate?.someDelegateMethod() // call delegate before popping
navigationController?.popViewController(animated: true)
}
}
One common way is to use delegate pattern. Pass the viewController back with some data using the delegate method and dismiss it from “parent” ViewController.
See these links for extra daya about delegates
link1
link2

Reload tableView after dismiss a viewController

I have a ViewController(VCA) with a TableView inside. From this ViewController it is possibile to call another ViewController (VCB). In this second VC it is possibile add an item to the plist used to populate the TableView in VCA. The problem is that when I save the new item and dismiss the VCB, I can't reload the TableView in VCA.
I have found a lot of examples:
How can you reload a ViewController after dismissing a modally presented view controller in Swift?
How to call reload table view after you call dismissViewController in swift?
How to reload tableview from another view controller in swift
Update the data of a TableViewController after add item
Update the first view after dismissing Popover
Table view is not getting updated after dismissing the popover?
after reading i tried with this code:
**IN VCB**
import UIKit
protocol AddItemDelegateProtocol {
func didAddItem()
}
class VCB: UIViewController {
var delegate : AddItemDelegateProtocol?
...
}
#IBAction func saveButton(_ sender: Any) {
....
self.delegate?.didAddItem()
dismiss(animated: true, completion: nil)
}
**In VCA**
class VCA: UIViewController, UITableViewDelegate, UITableViewDataSource, AddItemDelegateProtocol {
let addItemVC = VCB()
...
override func viewDidLoad() {
super.viewDidLoad()
addItemVC.delegate = self
...
}
func didAddItem() {
self.tableView.reloadData()
}
but this doesn't work. I don't
understand where I'm wrong. Could
you help me?
EDIT: my Solution
I solved in this way:
I've created a singleton in which I declare:
class DataManager {
static let shared = DataManager()
var firstVC = VCA()
.....
}
then, in viewDidLoad of VCA:
DataManager.shared.firstVC = self
now, in the saveButton of VCB, i can call:
#IBAction func saveButton(_ sender: Any) {
........
DataManager.shared.firstVC.tableView.reloadData()
dismiss(animated: true, completion: nil)
}
you can do this in two way :-
1)
Do One thing in VCA
VCA
override func viewWillAppear(_ animated: Bool){
tableView.reloadData()
}
If this does not work out then try this.
2)
create an instance of VCA in VCB and whenever you move from VCA to VCB pass the value of VCA to the instance of VCB and from there reload the table.
VCB
var instanceOfVCA:VCA! // Create an instance of VCA in VCB
func saveButton(){
instanceOfVCA.tableView.reloadData() // reload the table of VCA from the instance
dismiss(animated: true, completion: nil)
}
VCA
override func prepare(for segue: UIStoryboardSegue, sender: Any!) {
VCB.instanceOfVCA = self // Pass the value of VCA in instance of VCB while navigating through segue
}
Here you are calling table's reload data when the viewcontroller is not yet shown. i.e, Even before you dismissed the viewcontroler VCB and viewcontroller VCA is shown, you are calling reloadData.
Try calling reload data in VCA's viewWillAppear(animated: Bool) function.
Try this: make changes as below
let addItemVC : VCB? = nil
In ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
addItemVC = (storyboard?.instantiateViewController(withIdentifier: "ViewControllerID") as! SelectionViewController?)! // change ViewControllerID with your controller id
addItemVC.delegate = self
}
The code in your question is perfectly good. I use the same approach and it works like a charm.
IMHO the problem in your code is that you only refresh the table view with self.tableView.reloadData(), BUT may be you forget to refresh your data model - the data source for the table view. E.g. if you delete an entity from Core Data then you need to refetch your data and only after that reload the table view.
I managed to do it using delegate/protocol that is usually used to pass data between view controllers but in this instance I just called the function without passing data and inside this function i put ( tableView.reloadData() ) and it worked like a sweet :)
juts google "Passing data between view controllers and use the method as I explained above"

View hierarchy error while presenting a viewcontroller

I have a following class:
class HomeViewController: UIViewController {
override func viewdidload() {
super.viewdidload()
callOtherVC()
}
func callOtherVC() {
let viewController = StepsViewController()
let rootViewController = UINavigationController(rootViewController: viewController)
self.presentViewController(rootViewController, animated: true, completion: nil)
}
}
StepsViewController is just another viewcontroller. In StepsViewController, I try to dismiss current StepsViewController and present other viewcontroller. Following is code.
class StepsViewController: UIViewController {
override func viewdidload() {
super.viewdidload()
callSecondOtherVC()
}
func callSecondOtherVC() {
let vc = ViewController()
self.addChildViewController(vc)
self.parentViewController!.dismissViewControllerAnimated(true, completion: nil)
vc.callOtherVC()
}
}
I initialize ViewController() because I need to call same function callOtherVC from ViewController. Basically the model in ViewController changes but I'm essentially calling same UINavigationController from callOtherVC function.
Whenever I do this, I get an error like below:\
Warning: Attempt to present (UINavigationController: 0x7d991600) on
(HomeViewController: 0x7a6e00a0) whose view is not in the window
hierarchy!
UINavigationController is from callSecondOtherVC and HomeViewController is as it is.
How should I order the VCs? And if someone can more explain about the view hierarchy, I would greatly appreciate.
I think what you need to do here, is call your method from viewDidAppear, rather than viewDidLoad. The reason for this is that the view is not in the view hierarchy at the time of viewDidLoad.

What is the iOS equivalent of Android's startActivityForResult/setResult?

I am an Android programmer learning iOS. I have a swift view1 that calls an objective C view2. I am looking to return a String from view2 back to view 1.
In Android, we would simply view1.startActivityForResult(View2.class)
What is the iOS way to do this?
ViewController2.h - Objective-C
#protocol ViewController2Delegate <NSObject>
- (void)sendStringBack:(NSString *)aString;
#end
#interface ViewController2 : UIViewController
#property (nonatomic, weak) id<ViewController2Delegate> delegate;
#end
ViewController2.m - Objective-C
// When you want to send the string back and dismiss the view:
[self.delegate sendStringBack:theStringToSendBack];
ViewController1.swift - Swift
#objc class ViewController1: UIViewController, ViewController2Delegate {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let viewContr = segue.destinationViewController as! ViewController2
viewContr.delegate = self
}
func sendStringBack(aString: String) {
let aVariable = aString
// do something with the string
// dismiss the view if you presented it modally
self.dismissViewController(self, animated: true, completion: nil)
// OR
// dismiss the view if you presented it with show/push
self.navigationController?.popViewControllerAnimated(true)
}
}
There is nothing like startActvityForResult() in iOS. You have to do it manually. I did it with the following process.
ViewControllerOne has a form that needed data from viewControllerTwo. ViewControllerTwo has a tableView with dataList. By tapping a data on the tableView of ViewControllerTwo it will go back to ViewControllerOne with the table data that was tapped.
Create a global variable for storing data.
var data:String = ""
Open ViewControllerTwo from ViewControllerOne with segue.
In ViewControllerTwo which has a tablewView. In didSelectRowAt method set the data to global variable and Call the method
self.navigationController?.popViewController(animated: true) like below.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
data = dataList[indexPath.row]
self.navigationController?.popViewController(animated: true)
}
It will go back to ViewControllerOne. Now You have catch the data in viewWillAppear() or viewDidAppear() method. For example:
override func viewWillAppear(_ animated: Bool) {
if data != ""{
// Do whatever you want with this data. after doing your work set the data value to empty.
data = ""
}
}
Thats it. Happy Coding. It will work 100% sure
Use UIViewController+CallBack for this purpose.
Only need to set the resulting object from parent controller and child controller will override the protocol method and return any kind of object to the parent.
(id)viewUnloadWithResultObject
No ned to set the protocol
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"NewViewController"];
[controller setResultBlock:^(id resultObject) {
NSLog(#"New View Controller did pop and resulting object is: %#", resultObject);
}];
[self.navigationController pushViewController:controller animated:YES];
You can also present the view controller and get same resulting object
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"NewViewController"];
[controller setResultBlock:^(id resultObject) {
NSLog(#"New View Controller did dismissed and resulting object is: %#", resultObject);
}];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
[self.navigationController presentViewController:navController animated:YES completion:NULL];
Also you can have view unload call back methods. i.e.
(void)viewWillUnloadCallBack
(void)viewDidUnloadCallBack
Just inherit protocol and override these methods in your view controller to get the unload call backs.

Perform push segue after an unwind segue

I am working on a camera app where the camera views are shown modally. After I am done with cropping. I perform an unwind segue to the MainPageViewController. (Please see the screenshot)
My unwind function inside MainPageViewController is as follows;
#IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
self.performSegueWithIdentifier("Categories", sender: self)
}
where "categories" is the push segue identifier from MainPageViewController to CategoriesTableViewController.
The program enters the unwindToMainMenu function but it does not perform the push segue. Any idea how to fix this?
Note: I found the same question but the answer suggests to change the storyboard structure.
A bit late to the party but I found a way to do this without using state flags
Note: this only works with iOS 9+, as only custom segues support class names prior to iOS9 and you cannot declare an exit segue as a custom segue in storyboards
1. Subclass UIStoryboardSegue with UIStoryboardSegueWithCompletion
class UIStoryboardSegueWithCompletion: UIStoryboardSegue {
var completion: (() -> Void)?
override func perform() {
super.perform()
if let completion = completion {
completion()
}
}
}
2. Set UIStoryBoardSegueWithCompletion as the class for your exit segue
note: the action for this segue should be unwindToMainMenu to match the original question
3. Update your unwind #IBAction to execute the code in the completion handler
#IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
if let segue = segue as? UIStoryboardSegueWithCompletion {
segue.completion = {
self.performSegueWithIdentifier("Categories", sender: self)
}
}
}
Your code will now execute after the exit segue completes its transition
I want to provide my own solution to this problem for now. Any further answers are always welcome.
I put a boolean variable and viewDidAppear function to MainPageViewController.
var fromCamera = false
override func viewDidAppear(animated: Bool) {
if fromCamera {
self.performSegueWithIdentifier("categorySelection", sender: self)
self.fromCamera = false
}
}
I set fromCamera to true before I perform unwind segue from CropViewController. By that way, I perform segue to category screen only if an unwind segue from crop view is performed.
Taking forward this answer (I only had Objective-C code)
Subclass UIStoryBoardSegue
#import <UIKit/UIKit.h>
#interface MyStoryboardSegue : UIStoryboardSegue
/**
This block is called after completion of animations scheduled by #p self.
*/
#property (nonatomic, copy) void(^completion)();
#end
And call this completion block after completion of animations.
#implementation MyStoryboardSegue
- (void)perform {
[super perform];
if (self.completion != nil) {
[self.destinationViewController.transitionCoordinator
animateAlongsideTransition:nil
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if (![context isCancelled]) {
self.completion();
}
}];
}
}
#end
Taking forward the previous two answers, there is a bit more detail about the objective c version here (I too only had Objective-C code)
Subclass UIStoryboardSegue with UIStoryboardSegueWithCompletion
class UIStoryboardSegueWithCompletion: UIStoryboardSegue {
var completion: (() -> Void)?
override func perform() {
super.perform()
if let completion = completion {
completion()
}
}
}
UIStoryboardSegueWithCompletion.h
#import <UIKit/UIKit.h>
#interface MyStoryboardSegue : UIStoryboardSegueWithCompletion
#property (nonatomic, copy) void(^completion)();
#end
UIStoryboardSegueWithCompletion.m
#import "UIStoryboardSegueWithCompletion.h"
#implementation UIStoryboardSegueWithCompletion
- (void)perform {
[super perform];
if (self.completion != nil) {
[self.destinationViewController.transitionCoordinator
animateAlongsideTransition:nil
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if (![context isCancelled]) {
self.completion();
}
}];
}
}
#end
Set UIStoryBoardSegueWithCompletion as the class for your exit segue
note: the action for this segue should be unwindToMainMenu to match the original question
[image showing segue ui][1]
[image showing segue ui 2][2]
Select exit segue from storyboard Add custom class
-(IBAction)unwindToMainMenu(UIStoryboardSegue *)segue {
if([segue isKindOfClass:[UIStoryboardSegueWithCompletion class]]){
UIStoryboardSegueWithCompletion *segtemp = segue;// local prevents warning
segtemp.completion = ^{
NSLog(#"segue completion");
[self performSegueWithIdentifier:#"Categories" sender:self];
};
}
}
Your code will now execute after the exit segue completes its transition
I'm guessing the performSegue is not firing because the unwind segue has not yet finished. The only thing I can think of at the moment, is to delay calling the performSegue using dispatch_after. This seems very "hacky" to me though.
#IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
dispatch_after(1, dispatch_get_main_queue()) { () -> Void in
self.performSegueWithIdentifier("Categories", sender: self)
}
}
The exit segue IBAction method happens before the actual unwind segue is finished. I had the same issue and resolved it this way (if you don't mind my paraphrasing of your code). It avoids the extra time and animations from relying on ViewDidAppear.
#IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
let categoriesTable = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("CategoryTableViewController")
self.navigationController?.viewControllers.append(categoriesTable)
self.navigationController?.showViewController(categoriesTable, sender: self)
}
Hope this is helpful for anyone else who runs into this and just wants an instantaneous transition!
Updated #moride's answer for Swift 5. The transition coordinator is now optional, so we run completion immediately if this is the case.
class UIStoryboardSegueWithCompletion: UIStoryboardSegue {
var completion: (() -> Void)?
override func perform() {
super.perform()
guard let completion = completion else { return }
guard let coordinator = destination.transitionCoordinator else {
completion()
return
}
coordinator.animate(alongsideTransition: nil) { context in
guard !context.isCancelled else { return }
completion()
}
}
}

Resources