Passing NSIndexPath to New View - objective-c

I am having real troubles passing an NSIndexPath to my new view. This is how the app works:
I have a UIBarButtonItem in my nab br, tap that and you get a popover view, this shows a bunch of stuff. I need to get an NSIndexPath from my main view, to this popover view.
I have a property for the NSIndexPath in my popover view class and the popover transition is connected up in my storyboard.
Then I have this code to pass the index path across views:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"statsPopover"])
{
StatsViewController *statsVC = [segue destinationViewController];
statsVC.selectedIndex = stageSelectionTable.indexPathForSelectedRow;
}
}
However, while this gets called, the index path isn't actually sent between views. My index path on the popover is always the default, 0,0 row and section.

You say you know from debugging/logging that the method is running, your if statement is triggered, and stageSelectionTable.indexPathForSelectedRow has the value you expect.
Doing that kind of diagnosis puts you on the right path to solving your issue. Keep at it -- if you test some other things with NSLog or the debugger you should be able to find the problem.
Do you know that statsVC is non-nil? If it's nil, your message to set selectedIndex does nothing.
Is statsVC the class you expect? If, say, the (table?) view in your popover is embedded in a navigation controller, segue.destinationViewController will point to the nav controller, and you'll need to look into its child view controllers array to find the one you're looking for.
Is whatever accessor method your StatsViewController class is using for its selectedIndex property working right? (This shouldn't be a problem if it's a synthesized accessor for a strong/retain property.)
In your StatsViewController class, are you trying to work with selectedIndex before it actually gets set? Setting a property in prepareForSegue:sender: and then using it in viewDidLoad should work fine, but using it in awakeFromNib might not, and using it in an init... method definitely won't.

Related

Correct method to present a different NSViewController in NSWindow

I am developing an app that is a single NSWindow and clicking a button inside the window will present a NSViewController, and a button exists in that controller that will present a different NSViewController. I know how to swap out views in the window, but I ran into an issue trying to do this with the multiple view controllers. I have resolved the issue, but I don't believe I am accomplishing this behavior in an appropriate way.
I originally defined a method in the AppDelegate:
- (void)displayViewcontroller:(NSViewController *)viewController {
BOOL ended = [self.window makeFirstResponder:self.window];
if (!ended) {
NSBeep();
return;
}
[self.box setContentView:viewController.view];
}
I set up a target/action for an NSButton to the AppDelegate, and here's where I call that method to show a new view controller:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self displayViewcontroller:newVC];
}
This does work - it presents the new view controller's view. However if I then click any button in that view that has a target/action set up that resides within its view controller class, the app instantly crashes.
To resolve this issue, I have to change didTapContinue: to the following:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self.viewControllers addObject:newVC];
[self displayViewcontroller:[self.viewControllers lastObject]];
}
First of all, can you explain why that resolves the issue? Seems to be related to the way the controller is "held onto" in memory but I'm not positive.
My question is, how do I set this up so that I can swap out views from within any view controller? I was planning on getting a reference to the AppDelegate and calling displayViewcontroller: with a new controller I just instantiated in that class, but this causes the crash. I need to first store it in the array then send that reference into the method. Is that a valid approach - make the viewControllers array public then call that method with the lastObject, or how should this be set up?
What is interesting in your code is that you alloc/init a new view controller every time that you call the IBAction. It can be that your view its totally new every time you call the IBAction method, but I would think that you only have a limited number of views you want to show. As far as my knowledge goes this makes your view only to live as long as your IBAction method is long. That the view still exists, is because you haven't refreshed it. However, calling a method inside a view controller that is not in the heap anymore (since you left the IBAction method and all local objects, such as your view controller are taken of the heap thans to ARC) makes the app crash, because you reference a memory space that is not in use or used by something else.
Why does the app work when you ad the view to the viewcontrollers array? I assume this array is an array that has been initiated in the AppDelegate and now you add the view controller with a strong reference count to the viewcontrollers array. When you leave the IBAction method, the view controller still has a strong reference and ARC will not deallocate the view controller.
Is this the proper way? Well, it works. I would not think it is considered very good programming, since you don't alloc/init an object in a method that needs to stay alive after leaving the method. It would be better practice to allocate and initialize your view controller(s) somewhere in an init, awakeFromNIB or a windowDidLoad method of your AppDelegate. The problem with your current solution is that you are creating an endless array of view controllers of which you only use the last. Somewhere your program will feel the burden of this enormously long array of pretty heavy objects (view controllers) and will run out of memory.
Hope this helps.
By the way, this is independent of whether you use Mavericks or Yosemite. I was thinking in a storyboard solution, but that wouldn't answer your question.
Kind regards,
MacUserT

Object becomes Nil when pushing controller

I have a the first application controller, MAViewControllerMenu, and when that controller loads, I already allocate the next controller, imageControllerView.
- (void)viewDidAppear{
[super viewDidAppear:(YES)];
if (!imageControllerView)
imageControllerView = [[self storyboard] instantiateViewControllerWithIdentifier:#"chosenImageController"];
}
Then, I select an image from the image picker, and want to move to the next controller,imageControllerView, where the image would be displayed. I set the next window's image property as follows:
imageControllerView.image = [[self.pageViews objectAtIndex:(centered_image_ind)] image];
This line works, I checked that there's a value in imageControllerView.image.
However, when I move to the next controller,imageControllerView , I notice that the memory address of imageControllerView changes, or in other words, imageControllerView's properties that I change before moving to that controller, specifically image, reset when I move there.
Instead of throwing code here, I was hoping you could let me know what I should provide.
I think it's a common problem people know of:
Controller's objects re-init'ing when moving from one controller to another.
Here's a screen shot that might give a hint of what Im trying to do
Left most one is where I select pictures which in turn go into the slide show scrollview. Then I click next, and the image is supposed to appear in the centered ImageView
Thanks
OK...
You cannot "already allocate the next view controller" this won't work. There is no point in creating it like this at all. You can delete the imageViewController property (or iVar) completely.
The arrows that you have between the view controllers in your storyboard are segues. In Interface Builder you can select a segue and give it an identifier. For instance you would use something like #"ImageViewSegue".
I guess the segue is attached to the Next button. This is fine.
Now, in your MAViewControllerMenu you need to put this method...
- (void)prepareForSegue:(UIStoryBoardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"ImageViewSegue"]) {
// the controller is CREATED by the segue.
// the one you create in view did load is never used
ImageViewController *controller = segue.destinationController;
controller.image = [[self.pageViews objectAtIndex:(centered_image_ind)] image];
}
}
Now for the segues in the other direction...
You appear to be using segues to dismiss the modal views. You can't do this. What it will do is create a new view controller and present that instead of dismissing the presented view.
i.e. you'll go...
A -> B -> C -> B -> A -> B
// you'll now have 6 view controllers in memory
// each segue will create a fresh view controller with no values set.
What you want is...
A -> B -> C
A -> B
A
// now you only have one view controller because the others were dismissed.
// when you dismiss a view controller it will go back to the original one.
// the original one will have all the values you set previously.
To do this you need to create a method something like...
- (IBAction)dismissView
{
[self dismissViewControllerAnimated:YES completion:nil];
}
Then whatever the button is for your dismiss action attach it to this method.
Now delete all the backwards curly segues.
Passing info back
To pass info back to the original view controller you need a delegation pattern or something similar.
You can read more about creating a delegate at This random Google Search
Create a delegate method something like...
- (void)imageViewSelectedImage:(UIImage *)image;
or something like this.
Now when you do prepareForSegue you can do...
controller.delegate = self;
and have a method...
- (void)imageViewSelectedImage:(UIImage *)image
{
// save the method that has been sent back into an array or something
}
I might be wrong, but seems you go to your second view controller using a segue, it is normal your controller instance isn't the same than the one retrieved by [[self storyboard] instantiateViewControllerWithIdentifier:#"chosenImageController"]
you should take a look at - (void) prepareForSegue:(UIStoryboardSegue *)segue
(UIViewController method)
inside this method set your image property to the segue destination controller (check the identifier of the segue)

Replacing Storyboard Segue with pushViewController causes strange behaviour

I can't seem to figure this out for the life of me. I have a custom table view cell, in that cell I have a few buttons configured. Each button connects to other view controllers via a storyboard segue. I've recently removed these segues and put a pushViewController method in place. Transition back and forth across the various views works as expected however the destination view controller is not displaying anything! I have some code below as an example.
Buttons have this method set:
[cell.spotButton1 addTarget:self action:#selector(showSpotDetails:) forControlEvents:UIControlEventTouchUpInside];
// etc...
[cell.spotButton4 addTarget:self action:#selector(showSpotDetails:) forControlEvents:UIControlEventTouchUpInside];
// etc...
showSpotDetails Method contains this code:
- (void)showSpotDetails:(id)sender
{
// determine which button (spot) was selected, then use its tag parameter to determine the spot.
UIButton *selectedButton = (UIButton *)sender;
Spot *spot = (Spot *)[spotsArray_ objectAtIndex:selectedButton.tag];
SpotDetails *spotDetails = [[SpotDetails alloc] init];
[spotDetails setSpotDetailsObject:spot];
[self.navigationController pushViewController:spotDetails animated:YES];
}
The details VC does receive the object data.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"spotDetailsObject %#", spotDetailsObject_.name);
}
The NSLog method below does output the passed object. Also, everything in the details view controller is as it was. Nothing has changed on the details VC. It just does not render anything ever since I removed the segue and added the pushViewController method. Perhaps I am missing something on the pushViewController method? I never really do things this way, I try to always use segues...
Any suggestions?
Welcome to the real world. Previously, the storyboard was a crutch; you were hiding from yourself the true facts about how view controllers work. Now you are trying to throw away that crutch. Good! But now you must learn to walk. :) The key here is this line:
SpotDetails *spotDetails = [[SpotDetails alloc] init];
SpotDetails is a UIViewController subclass. You are not doing anything here that would cause this UIViewController to have a view. Thus you are ending up a with blank generic view! If you want a UIViewController to have a view, you need to give it a view somehow. For example, you could draw the view in a nib called SpotDetails.xib where the File's Owner is an SpotDetails instance. Or you could construct the view's contents in code in your override of viewDidLoad. The details are in the UIViewController documentation, or, even better, read my book which tells you all about how a view controller gets its view:
http://www.apeth.com/iOSBook/ch19.html
The reason this problem didn't arise before is that you drew the view in the same nib as the view controller (i.e. the storyboard file). But when you alloc-init a SpotDetails, that is not the same instance as the one in the storyboard file, so you don't get that view. Thus, one solution could be to load the storyboard and fetch that SpotDetails instance, the one in the storyboard (by calling instantiateViewControllerWithIdentifier:). I explain how to do that here:
http://www.apeth.com/iOSBook/ch19.html#SECsivc

prepareForSegue is not called after performSegue:withIdentifier: with popover style

I have a universal app, where I am sharing the same controller for a IPad and IPhone storyboard.
I have put a UILongPressGestureRecognizer on a UITableView, that when a cell is pressed on iPhone it calls an action that perform a segue:
-(IBAction)showDetail:(id)sender {
UILongPressGestureRecognizer *gesture = (UILongPressGestureRecognizer*)sender;
if (gesture.state == UIGestureRecognizerStateBegan) {
CGPoint p = [gesture locationInView:self.theTableView];
NSIndexPath *indexPath = [self.theTableView indexPathForRowAtPoint:p];
if (indexPath != nil) {
[self performSegueWithIdentifier:SEGUE_DETAIL sender:indexPath];
}
}
}
the segue is a detail view performed as a 'push'. The first thing you should notice is that the sender is an NSIndexPath, is the only way I found for passing the selected cell. Maybe there's a better solution.
Everything works fine, in a sense that the segue is performed, and before the prepareForSegue is called too.
However it happens that on iPad, I have changed the segue identifier to Popover.
Now things are working in part, the segue is performed, but prepareForSegue is not called and therefore the destination view controller is not set up as it should be.
What am I doing wrong ?
What I have discovered so far, is that with any segue identifier that is not popover these are the invocations made by iOS:
prepareForSegue (on source controller)
viewDidLoad (on destination controller)
while in popover segue the invocation order is:
viewDidLoad (on destination controller)
prepareForSegue (on source controller)
just because I put all my logic in viewDidLoad, the controller was not properly initialized, and a crash happened. So this is not exactly true that prepareForSegue is not called, the truth is that I was getting an exception, and I wrongly mistaken as prepareForSegue not getting called.
I couldn't put everything in viewWillAppear because a call to CoreData had to be made and I didn't want to check if entities were ok each time the view display.
How did I solve this ? I created another method in destination controller
-(void)prepareViewController {
// initialization logic...
}
and changing the prepareForSegue method in source controller itself:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
MyViewController *mvc = (MyViewController*)[segue destinationViewController];
// passing variable
// with segue style other than popover this called first than viewDidLoad
mvc.myProp1=#"prop1";
mvc.myProp2=#"prop2";
// viewWillAppear is not yet called
// so by sending message to controller
// the view is initialized
[mvc prepareViewController];
}
don't know if this is expected behavior with popover, anyway now things are working.
I've noticed that the boiler plate code for Xcode's Master-Detail template (iPhone) uses the following pattern for configuring the detail VC's view:
the detail VC's setters (for properties) are overwritten in order to invoke the configureView method (configureView would update all your controls in the view, e.g., labels, etc.)
the detail VC's viewDidLoad method also invokes the configureView method
I did not follow this pattern the other day when I was trying to re-use a detail VC in my movie app, and this gave me trouble.
I don't have much experience with popovers; however, if the pattern above is used with a detail VC that is displayed inside a popover, then wouldn't the detail VC's view get configured when you set the detail VC's properties from within the prepareForSegue method?

presentModalViewController on Parent from UITableView inside UIViewController

This one is probably something simple, still learning the ins-and-outs on this but I've run out of searches for this one with no available answer.
I've got a UIViewController with several elements displayed on it, one such element is a UITableView. The UITableView has it's own class and is allocated in the UIViewControllers viewWillAppear
- (void)viewWillAppear:(BOOL)animated
{
UITableView *insideTableView = [[UITableView alloc] init];
tableView.delegate = insideTableView;
tableView.dataSource = insideTableView;
}
Everything is working fine in regards to the tableview. Today I am experimenting with a few additions, one of which is a new view popup on cell selection within that tableview.
Inside my TableView Class, I have the following:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"Cell Pressed, Present View");
PopupView *popupView = [[PopupView alloc] initWithNibName:#"PopupView" bundle:nil];
popupView.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:popupView animated:YES];
}
Now it gets called fine, verified by the NSLog, however the view doesn't appear. I know the problem is related to the fact that I want PopUp to appear over the TableViews Parent rather than itself.
I'm just not sure how to properly call it in this instance.
The delegate is a UIViewController which doesn't have its view property set, which is why presentModalViewController:: doesn't work.
You need the view controller containing the table view to present the modal view controllers, but note that that view controller is not the parent of the table view delegate. This is because you have no view controller hierarchy in place.
The easiest way to fix this is to put those methods inside the view controller whose view contains the table view. Alternatively the table view delegate needs to hold a reference to the view controller so it can call presentModalViewController:: on it.
The latter approach can lead to retain cycle, so you have to use a non-retaining reference. The nicest implementation is the delegate pattern.
Also, you don't want to do the instantiation in viewWillAppear: because that can be called multiple times during the lifecycle of a view controller. Put the code in viewDidLoad and balance it in dealloc. Right now you are leaking memory every time your view appears, which when your modal view controller is working will be every time the modal view controller is presented and dismissed.