iOS: popViewController unexpected behavior - xcode

I've been searching the internet for a solution. There's nothing I could find.
So:
I'm using a UINavigationController. I am pushing two UIViewControllers onto it. In the second pushed ViewController i am executing this code:
- (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFailWithError:(NSError *)error {
NSLog([error localizedDescription]);
[self.navigationController popViewControllerAnimated:YES]; }
The expected thing to happen would be that the last pushed ViewController disappears. In this app I am doing this on few places and it works fine everywhere expect in this very ViewController.
What happens is that only the back button goes off screen (animated) but everything else stays on screen. In the Console Output two things are printed out when this line executes:
2011-03-14 16:32:44.580
TheAppXY[18518:207] nested pop
animation can result in corrupted
navigation bar
2011-03-14 16:32:53.507
TheAppXY[18518:207] Finishing up a
navigation transition in an unexpected
state. Navigation Bar subview tree
might get corrupted.
Two error messages I couldn't find ANY information on.
I'm using XCode 4 and iOS SDK 4.3. Maybe anyone can help me with this problem.

I came across a similar situation in my code and the message said:
nested push animation can result in corrupted navigation bar
Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree >might get corrupted.
My finding to this issue was that I was pushing 2 view controllers one after the other in quick succession and both were animated.
In your case it seems that you might be popping multiple view controllers with animation one after the other.
Hence, while one view is undergoing animation you should not start animation on another view.
I also found that if I disabled animation on one view, the error message disappeared.
In my case it was a problem with the flow logic as I did not intend to push 2 view controllers one after the other. One was being pushed within the switch case logic and another after its end.
Hope this helps someone.

You can get this anytime that you try to pop before viewDidAppear. If you set a flag, then just check that flag in viewDidAppear, you wont have a problem.

I have created a drop-in replacement for UINavigationController that will queue animations for you and avoid this problem entirely.
Grab it from BufferedNavigationController

I had this problem, too, and here's what was causing mine:
In RootViewController, I am using several UISegmentedControl objects to determine which of many views to load next.
In that (sub/2nd) view, I was popping (by using the "Back" button) back to RootViewController.
In RootViewController, I was handling viewWillAppear to "reset" each of my UISegmentedControl objects to a selectedSegmentIndex of -1 (meaning no segment looks "pressed").
That "reset" triggered each of my UISegmentedControl objects to fire their associated (and separate) IBActions.
Since I wasn't handling for a "selection" of -1, I had multiple methods firing at the same time, all trying to push a different view.
My fix? I tightened up my if...then statements and bailed on executing any code in my UISegmentedControl IBActions when selectedSegmentIndex == -1.
I'm still not sure why I got "pop" animation errors and not "push" errors, but at least figured out my error and got it fixed!
Hope this helps someone else!

yeah, unfortunately apple did not synchronize UINavigationController's animations. Andrew's solution is excellent, but if you don't want to cover its whole functionality, there is a simpler solution, override these two methods :
// navigation end event
- ( void ) navigationController : ( UINavigationController* ) pNavigationController
didShowViewController : ( UIViewController* ) pController
animated : ( BOOL ) pAnimated
{
if ( [ waitingList count ] > 0 ) [ waitingList removeObjectAtIndex : 0 ];
if ( [ waitingList count ] > 0 ) [ super pushViewController : [ waitingList objectAtIndex : 0 ] animated : YES ];
}
- ( void ) pushViewController : ( UIViewController* ) pController
animated : ( BOOL ) pAnimated
{
[ waitingList addObject : pController ];
if ( [ waitingList count ] == 1 ) [ super pushViewController : [ waitingList objectAtIndex : 0 ] animated : YES ];
}
and create an NSMutableArray instance variable called waitingList, and you are done.

This problem happen with me when i use storyboards. I've made a mistake:
I have a UIButton with an action to performSegueWithIdentifier. So i link the push segue with Button with the other ViewController so occur this problem.
To solve:
Link the button action in UIButton and link the push segue between two ViewControllers.

Combining MilGra and Andrew's answers gave me something that works reliably and is a simpler drop-in UINavigationController replacement.
This improves on MilGra's answer to make it work with pushes and pops, but is simpler than Andrew's BufferedNavigationController. (Using BufferedNavigationController I was occasionally getting transitions that would never complete and would only show a black screen.)
This whole thing seems not to be necessary on iOS8, but was still needed for me on iOS7.
#implementation UINavigationControllerWithQueue {
NSMutableArray *waitingList;
}
-(void) viewDidLoad {
[super viewDidLoad];
self.delegate = self; // NOTE: delegate must be self!
waitingList = [[NSMutableArray alloc] init];
}
# pragma mark - Overrides
-(void) pushViewController: (UIViewController*) controller
animated: (BOOL) animated {
[self queueTransition:^{ [super pushViewController:controller animated:animated]; }];
}
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
UIViewController *result = [self.viewControllers lastObject];
[self queueTransition:^{ [super popViewControllerAnimated:animated]; }];
return result;
}
- (NSArray*)popToRootViewControllerAnimated:(BOOL)animated {
NSArray* results = [self.viewControllers copy];
[self queueTransition:^{ [super popToRootViewControllerAnimated:animated]; }];
return results;
}
# pragma mark - UINavigationControllerDelegate
-(void) navigationController: (UINavigationController*) navigationController
didShowViewController: (UIViewController*) controller
animated: (BOOL) animated {
[self dequeTransition];
}
# pragma mark - Private Methods
-(void) queueTransition:(void (^)()) transition {
[waitingList addObject:transition];
if (waitingList.count == 1) {
transition();
}
}
-(void) dequeTransition {
if (waitingList.count > 0) {
[waitingList removeObjectAtIndex:0];
}
if (waitingList.count > 0) {
void (^transition)(void) = [waitingList objectAtIndex:0];
if (transition) {
transition();
}
}
}
#end

Related

scrollRectToVisible UITextField doesn't scroll with Autolayout

[self.scrollView scrollRectToVisible:textField.bounds animated:YES];
I can't seem to get my UIScrollView to scroll at all so that it doesn't obscure my UITextField. I thought that scrollRectToVisible would be my savior but it looks like a no go. Maybe I'm missing something like translating the coordinates of my textField to my scrollView. Either way check out my sample project.
https://github.com/stevemoser/Programming-iOS-Book-Examples/tree/master/ch20p573scrollViewAutoLayout2
Oh, and this project might be missing the delegate connection but I checked that and it still doesn't scroll.
I've seen other questions similar to this but none that mention Autolayout.
I was having issues with scrollRectToVisible:: as well after converting to Auto Layout. I just changed it to a direct call to setContentOffset:: and it started working again.
I had the same problem, I wanted to scroll an autolayouted UITextEdit into view without making it the first responder.
For me the issue was that the bounds of the UITextField were set later on during the auto layout pass, so if you do it immediately after setting up the layout the bounds are not valid yet.
To workaround I did create a descendant of UITextField, did overwrite setBounds: and added a 0 timer to scroll into view "later on" (You can't scroll in that moment because the auto layout pass of the system might no be finished at that point)
#interface MyTextField: UITextField
{
bool _scrollIntoView;
}
..
#end
#implementation MyTextField
-(void)setBounds:(CGRect)bounds
{
bool empty=CGRectIsEmpty(self.bounds);
bool isFirstResponder=self.isFirstResponder;
[super setBounds:bounds];
if (empty && !isFirstResponder && _scrollIntoView)
[self performSelector:#selector(scrollIntoViewLater) withObject:nil afterDelay:0];
else if (empty && isFirstResponder)
[self performSelector:#selector(becomeFirstResponder) withObject:nil afterDelay:0];
}
-(void)scrollIntoViewLater
{
CGRect r=[scrollView convertRect:self.bounds fromView:self];
[scrollView scrollRectToVisible:r animated:TRUE];
}
#end
If the field should be additionally editable with the on screen keyboard, simply call becomeFirstResponder later on: it scrolls automagically into view above the keyboard using the private scrollTextFieldToVisible API which in turn calls scrollRectToVisible:animated: of the scrollview.
Your sample link is broken btw...

UITableView becomes unresponsive

UITableViewCell becomes unresponsive this was a very different problem with a very different solution.
My tableView which is a subView in a UIViewController initially works fine and I can select individual rows in the table. However, I have created my own popup when a row is selected (the popup is a UIView) that appears towards the bottom of the screen. As this pops-up I also create a another UIView which covers the screen behind the popup and it makes the background go dim. The third thing that happens is that i create a UITapGestureRecogniser to keep track of the user's taps, and if they tap outside the UIView then the two UIViews and the TapGestureRecogniser are removed and call the deselectRowAtIndex... method.
However, it is at this point that I cannot use the tableView, as i want to be able to select a different string within the tableView and the popup to appear again (the popup will eventually contain links that will enable the user to move to different viewControllers).
I have tried to reload the data, remove the tableview and replace it, edit the didSelectRowAtIndex, remove the deselectRowAtIndex method, however nothing I tried seems to work and i can't find anything on stackoverflow as my question seems to be quite specific (although I apologise if there is something out there).
I'll add a few parts of my code in, however, I'm not sure where the problem is and I may not have copied the right part in.
The remove overhead is the selector method from the tapGesture
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(_popOverView == nil)
{
_popOverView = [[UIView alloc]initWithFrame:CGRectMake(20, 200, 280, 150)];
_popOverView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"wood.jpeg"]];
}
if(_mask == nil)
{
_mask = [[UIView alloc] initWithFrame:self.view.frame];
[_mask setBackgroundColor:[UIColor colorWithWhite:0.0 alpha:0.78]];
}
if (_tapDetector == nil)
{
_tapDetector= [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(removeOverHead:)];
}
[self.view addSubview:_mask];
[self.view addSubview:_popOverView];
[self.view addGestureRecognizer:_tapDetector];
}
-(void) removeOverHead:(UITapGestureRecognizer*) sender
{
CGPoint locationOfTap = [_tapDetector locationInView:self.view];
if (locationOfTap.y < (200 + 150) && locationOfTap.y > 200 && locationOfTap.x > 20 && locationOfTap.x < (20 + 280) ) {
NSLog(#"%f,%f",[_tapDetector locationInView:self.view].x,[_tapDetector locationInView:self.view].y);
}
else
{
[_mask removeFromSuperview];
[_popOverView removeFromSuperview];
[_tapDetector removeTarget:self action:#selector(removeOverHead:)];
/*this idea doesn't work :(
[self.tableView removeFromSuperview];
[self.view addSubview:_tableView];*/
}
}
I really hope the answer is in here and is very simple, and thank you in advance for taking the time to read this.
Solved it! Sorry for wasting your time. It was the wrong remove method for the gestureRecogniser. I replaced
[_tapDetector removeTarget:self action:#selector(removeOverHead:)]
with
[self.view removeGestureRecognizer:_tapDetector]
as the UIGestureRecogniser was lingering and obstructing the tableView!!
If you stick a breakpoint or NSLog() inside the else block of that remove method, do you get inside it?
It sounds like your if statement might be off. You should use CGRectContainsPoint(). However if I understand correctly, you're attempting to dismiss everything when the user taps the dimming background view. You could make this view a button or you could compare the touch's view pointer to the pointer to the background view.

UIPopoverViewController Adding Subviews is slow

I have a UIPopoverViewController that is displaying a custom UIViewController properly. When I click a button I have an action run and as a result I add a view to the view hierarchy of the UIViewController's view.
The problem is that it is very slow, and takes several seconds for the view to appear. I'm not doing anything out of the ordinary with my UIViewController's code.
- (void)showAccountChooser {
self.twitterAccountPicker = [TwitterAccountPicker new];
[self.view addSubview:self.twitterAccountPicker.view];
self.twitterAccountPicker.view.frame = self.view.bounds;
self.twitterAccountPicker.view.transform = CGAffineTransformMakeScale(.05, .05);
[UIView animateWithDuration:0.5f animations:^{
self.twitterAccountPicker.view.transform = CGAffineTransformMakeScale(1, 1);
} completion:^(BOOL finished) {
//[self.twitterAccountPicker viewDidAppear:YES];
}];
}
The UIViewController that I'm adding is trivial and does not heavy processing in the viewDidLoad or viewWill/DidAppear. I have set break points and verified that it is not doing anything bad.
Anyone else notice this when adding views?
After setting break points trying to debug this, I realized that my showAccountChooser method was being called from a block invoke, which was happening on a background thread. Moving this call to the main thread resolved the issue.

How to stop the animation on a determinate NSProgressIndicator?

NSProgressIndicator allows stopAnimation: when isIndeterminate == YES, but how does one stop the animation for determinate progress bars?
For context, the progress bar I am trying to do this with is a child of an NSView, which itself is the view property of an NSMenuItem. Sending ridiculously high numbers to setAnimationDelay: does what I want, but only temporarily -- when the parent menu is closed and re-opened, the progress bar is animated again.
(Possibly unnecessary disclaimer: I swear this is a legit use case; I have to be able to visually (i.e.: without using text) display the progress of very-long-running tasks which may pause and re-start as needed by the backend. Answers which boil down to "UI design: ur doin it rong" will be not be accepted unless accompanied by a brilliant alternative suggestion. ;) )
Subclass NSProgressIndicator like this, it also works with Lion:
#interface UnanimatedProgressIndicator : NSProgressIndicator {
#private
BOOL isAnimating;
}
#end
#interface NSProgressIndicator (HeartBeat)
- (void) heartBeat:(id)sender; // Apple internal method for the animation
#end
#implementation UnanimatedProgressIndicator
- (void) startAnimation:(id)sender
{
isAnimating = YES;
[super startAnimation:sender];
}
- (void) stopAnimation:(id)sender
{
isAnimating = NO;
[super stopAnimation:sender];
}
- (void) heartBeat:(id)sender
{
if (isAnimating)
[super heartBeat:sender];
}
#end
Solved the problem using the same approach as described previously, with one small change.
When the menu's delegate receives menuNeedsUpdate:, I send setAnimationDelay: (with the sufficiently huge number as the arg) to each progress bar. This is working reliably now, so I'm happy enough with it.

Custom NSView in NSMenuItem not receiving mouse events

I have an NSMenu popping out of an NSStatusItem using popUpStatusItemMenu. These NSMenuItems show a bunch of different links, and each one is connected with setAction: to the openLink: method of a target. This arrangement has been working fine for a long time. The user chooses a link from the menu and the openLink: method then deals with it.
Unfortunately, I recently decided to experiment with using NSMenuItem's setView: method to provide a nicer/slicker interface. Basically, I just stopped setting the title, created the NSMenuItem, and then used setView: to display a custom view. This works perfectly, the menu items look great and my custom view is displayed.
However, when the user chooses a menu item and releases the mouse, the action no longer works (i.e., openLink: isn't called). If I just simply comment out the setView: call, then the actions work again (of course, the menu items are blank, but the action is executed properly). My first question, then, is why setting a view breaks the NSMenuItem's action.
No problem, I thought, I'll fix it by detecting the mouseUp event in my custom view and calling my action method from there. I added this method to my custom view:
- (void)mouseUp:(NSEvent *)theEvent {
NSLog(#"in mouseUp");
}
No dice! This method is never called.
I can set tracking rects and receive mouseEntered: events, though. I put a few tests in my mouseEntered routine, as follows:
if ([[self window] ignoresMouseEvents]) { NSLog(#"ignoring mouse events"); }
else { NSLog(#"not ignoring mouse events"); }
if ([[self window] canBecomeKeyWindow]) { dNSLog((#"canBecomeKeyWindow")); }
else { NSLog(#"not canBecomeKeyWindow"); }
if ([[self window] isKeyWindow]) { dNSLog((#"isKeyWindow")); }
else { NSLog(#"not isKeyWindow"); }
And got the following responses:
not ignoring mouse events
canBecomeKeyWindow
not isKeyWindow
Is this the problem? "not isKeyWindow"? Presumably this isn't good because Apple's docs say "If the user clicks a view that isn’t in the key window, by default the window is brought forward and made key, but the mouse event is not dispatched." But there must be a way do detect these events. HOW?
Adding:
[[self window] makeKeyWindow];
has no effect, despite the fact that canBecomeKeyWindow is YES.
Add this method to your custom NSView and it will work fine with mouse events
- (void)mouseUp:(NSEvent*) event {
NSMenuItem* mitem = [self enclosingMenuItem];
NSMenu* m = [mitem menu];
[m cancelTracking];
[m performActionForItemAtIndex: [m indexOfItem: mitem]];
}
But i'm having problems with keyhandling, if you solved this problem maybe you can go to my question and help me a little bit.
Add this to your custom view and you should be fine:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return YES;
}
I added this method to my custom view, and now everything works beautifully:
- (void)viewDidMoveToWindow {
[[self window] becomeKeyWindow];
}
Hope this helps!
I've updated this version for SwiftUI Swift 5.3:
final class HostingView<Content: View>: NSHostingView<Content> {
override func viewDidMoveToWindow() {
window?.becomeKey()
}
}
And then use like so:
let item = NSMenuItem()
let contentView = ContentView()
item.view = HostingView(rootView: contentView)
let menu = NSMenu()
menu.items = [item]
So far, the only way to achieve the goal, is to register a tracking area manually in updateTrackingAreas - that is thankfully called, like this:
override func updateTrackingAreas() {
let trackingArea = NSTrackingArea(rect: bounds, options: [.enabledDuringMouseDrag, .mouseEnteredAndExited, .activeInActiveApp], owner: self, userInfo: nil)
addTrackingArea(trackingArea)
}
Recently I needed to show a Custom view for a NSStatusItem, show a regular NSMenu when clicking on it and supporting drag and drop operations on the Status icon.
I solved my problem using, mainly, three different sources that can be found in this question.
Hope it helps other people.
See the sample code from Apple named CustomMenus
In there you'll find a good example in the ImagePickerMenuItemView class.
It's not simple or trivial to make a view in a menu act like a normal NSMenuItem.
There are some real decisions and coding to do.

Resources