I've been converting an application to use storyboards. I'm sure this is a simple problem, but somehow I can't seem to figure out the 'correct' way of doing it, coming as we are from the old XIB world.
One of the subsections of it contains a UITabBarController, each with some subviews within it.
The action that launches this set of tabs works perfectly; I detect the segue, and set some data properties within my (custom) UITabBarController.
Next, I would like to be able to pass that data to the child views when they get created. But - because these tabs are simply 'relationships' and not segue's, I can't do what I do everywhere else, which is override the 'prepareForSegue' function.
In the old XIB universe, I'd simply bind some IBOutlets together between the tab controller and the child views. But I can't do that in storyboards, because the parent and children are separate 'scenes'.
I've tried making my UITabBarController class implement its own delegate, override 'didSelectViewController' and doing 'self.delegate = self' which almost works, except for the fact that it is never called with the first tab when the view is initially shown.
What's the "correct" (or 'best') way to do this? Please don't tell me to get/set some value on the app delegate, as this is 'global variable' territory - nasty.
Try looping through the view controllers on the UITabBarController, e.g. in this example the setData method is called from the segue in to the UITabBarController, and it then loops through the child view controllers, making a similar call on the child controller to set the data on that too;
- (void)setData:(MyDataClass *)newData
{
if (_myData != newData) {
_myData = newData;
// Update the view.
[self configureView];
}
}
- (void) configureView {
for (UIViewController *v in self.viewControllers)
{
if ([v isKindOfClass:[MyDetailViewController class]])
{
MyDetailViewController *myViewController = v;
[myViewController setData:myData];
}
}
}
Related
I have an iOS app that displays multiple screens and has different root controller for both iPhone and iPad. Here is a simplified example code to show what is going on.
if (iPad) {
self.sideMenuController = [LGSideMenuController sideMenuControllerWithRootViewController:viewControllerA
leftViewController:viewControllerB
rightViewController:nil];
} else {
self.sideMenuController = [LGSideMenuController sideMenuControllerWithRootViewController:viewControllerB
leftViewController:viewControllerA
rightViewController:nil];
}
[self.navigationController pushViewController:self.sideMenuController animated:NO];
I need to be able to tell when both controllers (ViewControllerA and ViewControllerB) is loaded.
I've implemented the following delegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.viewControllerA.viewIfLoaded.window != nil && self.viewControllerB.viewIfLoaded.window != nil) {
// do stuff after both controllers have been loaded and it is current view.
}
}
The delegate solution works, but not sure if it is best practice. I check if viewControllerA and viewControllerB is not nil and current view controllers because I push other controllers in the navigation controller and don't want to do anything if that happens.
It seems fragile. You're making a lot of assumptions and not (as far as I can tell) asking the navigation controller the question you really want to know the answer to. That question would be (as far as I can tell):
Is the view controller that just got pushed one of VCA and VCB?
Is the other of that pair already the navigation controller's child?
Assuming that the root view controller always loads first, you could perform work in the viewDidLoad method of the non-root view controller, which you could determine by referencing the device type.
You can have some BaseClass that both viewControllers inherit from (ViewControllerA and ViewControllerB) and in that BaseClass, you can use viewDidLoad: method to run whatever code you want.
I have a UITabBarController with Tabs (say Tab1, Tab2, Tab3, Tab4) and UITabBarController is my RootViewController and I'm making an API Call in the same. Since it is a RootViewController I'm displaying Tab1 as my default View. When I get the results to my API Call in UITabBarController. I need to share the details in real time to my Tabs. I have tried few ideas(like NotificationCenter, Singleton Class) but it's not working out. Can somebody help me to fix this? Thanks in advance. If you have a working example I kindly request you to share it with me.
Img Representation:
UITabBarController has a property viewControllers that is an array of view controllers displayed (by index for tab position, but you probably don't need that). One simple solution is when your UITabBarController subclass gets data, it can loop over the viewControllers array and check each for delegate conformance / responds to selector and update accordingly.
That's probably the simplest. Another way would be to have the view controllers in the tabs register for updates. So they conform to a delegate protocol, and get a reference to their tab bar controller (the tab bar controller subclass), and call the tab bar saying "observe updates." The tab bar controller keeps storage of registered observing objects and calls each with new data.
This assumes you are setting the tab's view controllers in IB. If you are doing it programmatically, then with the second option you can just link the tabs to the tab bar controller when you add them. If set in IB you could also do it by overriding -prepareForSegue since embedding the tabs in the root is considered a segue, but you'd still have to cast the destinationViewController as whatever subclass can receive data.
The first option is simple enough and though I don't like casting, it's unavoidable to take advantage that the references you want are right there. Plus, there will in reality only ever be a single-digit number of tabs, so it can't get expensive. Hope this helps.
This is just same like passing the values to another viewController but here you need to use tabBarController's delegate method in below example:
Passing value using delegate method from controller A:
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
ControllerB *controllerB = (ControllerB *) [tabBarController.viewControllers objectAtIndex:1];
//In this example, there is only have 2 tab bar controllers (A and B)
//So, index 1 is where controller B resides and index 0 where controller A resides.
self.controllerB.data.text = #"some value!";
//This will change the text of the label in controller B
}
Getting value in controller B
// set property in controller B .h file
#property(strong, nonatomic) UILabel *data;
// in controller B .m file viewDidLoad method
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"getting data: %#",data.text); // getting value
}
How about
-Declare a delegate protocol, say, Tab1ViewControllerDelegate, with a method - (void) tab1ViewController:(Tab1ViewController *) tab1ViewController, didReceiveData: (NSDictionary *) data
make a TabBarController subclass a delegate of your tab1ViewController
when tab1ViewController gets it's data, call [self.delegate tab1ViewController: self, didReceiveData: datDict]
then your TabBarController can distribute the data to its array of viewControllers
This is puzzling me.
The context
The original tutorial I'm following.
Where the segue is added to the Main View via a custom segue:
- (void) perform {
MainViewController *source = (MainViewController *)self.sourceViewController;
UIViewController *destination = (UIViewController *) self.destinationViewController;
for(UIView *view in source.main.subviews){
[view removeFromSuperview];
}
source.currentViewController = destination;
destination.view.frame = CGRectMake(0, 0, source.main.frame.size.width, source.main.frame.size.height);
[source.main addSubview:destination.view];
}
The TextField is connected as delegate in the child View Controller. All things being equal I get the app crashed without any message.
The workaround
In the Main View Controller, in -(void)prepareForSegue: I've added [segue.destinationViewController setDelegate:self]; in the meantime I've added a property in the child View Controller id<UITextFieldDelegate> delegate and modified the textfield delegate as self.delegate.
This works, but the trouble is that I've to set the delegated methods in Main View Controller which is not quite efficient as I have more View Controllers to add.
The Objective
How do I set each View Controller to be the delegate for itself without crashing?
The immediate cause of your error is that the view controller that your views belong to is being deallocated. The fact that your views are on screen while their view controller is deallocated highlights a fundamental flaw in the approach of taking views off one view controller and adding them to another. View controller containment is the correct way to solve an issue like this.
Changing the currentViewController property to strong will fix the memory management issue you're seeing, but it's just a bandaid. Your currentViewController will still be missing rotation methods, appearance and disappearance methods, layout methods, and so forth. View controller containment ensures these methods get called for the view controller whose views are on screen.
Here is an altered version of your project that illustrates how to use view controller containment. I think that will be a better solution than manually removing and adding subviews of the view controllers themselves. See the Apple docs for more info on custom view controller containers.
At first, let's see crash report. Please, do the following:
1. Add Exception Breakpoint
2. Edit it as in the picture
You should create a custom class for the destinationViewController wich will implement UITextFieldDelegate
#interface DestinationViewController <UITextFieldDelegate>
#end
And from storyboard add the class to UIViewController that has TextField
And make the connections for elements and TextField delegate.
Implement delegate methods.
You will not need the implementation of prepareForSegue: anymore. You will have two different classes with different elements. Only if you need to pass something from source to destination then you use prepareForSegue:
Hope you'll understand
I have a TabBarController with 4 tabs setup in storboard editor. When I select the 4th tab, I want to send a string to its view controller. In my TabbarController class (named TabBarVC) I implemented the following method:
- (void)tabBarController:(UITabBarController *)theTabBarController didSelectViewController:(UIViewController *)viewController {
NSUInteger indexOfTab = [theTabBarController.viewControllers indexOfObject:viewController];
if(indexOfTab == 3) {
SupplierListVC *slvc = (SupplierListVC *) viewController;
slvc.locationType = #"favorite";
self.tabBarController.selectedViewController = slvc;
}
}
This method is being called just fine but the string is not getting passed. When I debugged, I noticed that the above piece of code is called after viewDidLoad of SupplierListVC. What am I doing wrong here? How can I pass the string when I select the tab?
As you said, this method is called after viewDidLoad. The method name itself should tell you that: **DID**SelectViewController
Past tense. English may not be your first language, but a good understanding of English verb tense goes a really, really, really, really, really long way when working with Apple's method names. They're not misleading. They tell you exactly when/what/why that method is for, just in the method name.
Now then, the string SHOULD be appropriately passed into the view controller--you're probably just checking it at the wrong time.
But with a tab bar, the view controllers it contains aren't loaded each time that tab is switched to. The tabs are preloaded once and will only ever be unloaded if the tab bar controller itself is dismissed (or perhaps the view controller is removed from the tab bar controller's view controllers array).
You can use tabBarController:shouldSelectViewController:
This method, like the one you're using, gives you a reference to the tab bar controller and the view controller it's trying to switch too. It returns a BOOL value however. If you return NO the switch won't happen. You can simply always return YES if you want, the main point here though is that this method is called before the switch starts and therefore definitely before viewWillAppear and viewDidAppear are called.
I had the same issue when passing data from tabbar controller to viewcontroller.
NumberpadViewController *numpad= [[self viewControllers] objectAtIndex:1];
numpad.number=#"12345";
I had get number as nil in viewdidload method.
- (void)viewDidLoad {
NSLog(#"number#",number);//nil value
}
So I had used viewdidappear method to perfom processing on the number data.
-(void)viewDidAppear:(BOOL)animated{
NSLog(#"number#",number);//12345
}
I am writing an iOS7 app in Xcode 5 with storyboards. In a part of the app, I need three screens that shared the same viewController class. These screens are UIViewControllers. I use a UISegmentControl to go from screen to screen based on conditions. I disabled the control if the user had not completed certain steps.
I used a BOOL value check if certain steps had been completed and set its value to YES / NO.
The problem is when I want to go back to the last screen - I am getting a new instance of my viewController class. This has two problems:
Memory grows each time the user goes between the two views
BOOL value and all other properties are nil when new instance loads.
In my segment control this is how I get to the views:
-(void)segmentcontrol:(UISegmentedControl *)segment
{
if (segment.selectedSegmentIndex == 0)
{
self.viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"stepOne"];
[self presentViewController:self.viewController animated:NO completion:nil];
}
else if (segment.selectedSegmentIndex == 1 ){
self.viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"stepTwo"];
[self presentViewController:self.viewController animated:NO completion:nil];
}else {
}
}
This viewController is a subclass of my BaseViewController - which I used for UI elements that is constant across all screens.
What I want to do is return the same instance of the viewController class when I change the segment control to a another view, using the same class.
Is this at all possible?
Not clear why you're using presentViewController:animated:completion: but it looks like you're doing things in the wrong way.
What you want to be doing is creating a container controller. So, the view controller which hosts the segmented control creates a number of view controller instances and adds them as child view controllers. Now, when the segments are selected you get the child at the selected index, remove the old view controllers view from its superview and add the new view controllers view as a subview.
You don't need to do it like that, but it will probably be cleanest. Your memory grows currently because you use instantiateViewControllerWithIdentifier:. All you really need to do is to keep an array of view controllers and reuse instead of recreating. That said, continually presenting view controllers is not wise.