Objective C: Can I set a subview to be firstResponder? - objective-c

I have a situation whereby I am adding a view from another viewcontroller to an existing viewcontroller. For example:
//set up loading page
self.myLoadingPage = [[LoadingPageViewController alloc]init ];
self.myLoadingPage.view.frame = self.view.bounds;
self.myLoadingPage.view.hidden = YES;
[self.view addSubview:self.myLoadingPage.view];
Is it possible to set 'self.myLoadingPage' to be the first responder? This is the case whereby the loadingpage view size does not cover the entire size of the existing view and users can still interact with the superview (which is not the desired behaviour). I want to just enable the subview in this case.

When I had a similar problem, I made an invisible UIView that covered the entire screen, I added the large invisible UIView on top of the main view and made the loading view a subview of the invisible UIView.

The simplest solution is to override hitTest method in your loading view to return TRUE. This top view is first in the responder chain, the hitTest method gets called which NORMALLY returns TRUE if the point is within the view and will therefore be handled, returning TRUE regardless means you get the touch event and effectively block the message being resent to the next responder.

Interesting question. I found a similar post with a quote from the Apple Developer Forums on this issue:
To truly make this view the only thing
on screen that can receive touches
you'd need to either add another view
over top of everything else to catch
the rest of the touches, or subclass a
view somewhere in your hierarchy (or
your UIWindow itself) and override
hitTest:withEvent: to always return
your text view when it's visible, or
to return nil for touches not in your
text view.
This would seem to indicate there isn't a terribly straightforward solution (unless there was an API change regarding this made after October, 2010.)
Alternatively, I suppose you could go through all the other subviews in your superview and individually set their userInteractionEnabled properties to NO (but that would probably prove more cumbersome than the quoted solutions).
I would love to see other ways to allow this.

Related

Layer hosting NSView within NSOutlineView

I am trying to create a custom NSView that hosts a CALayer hierarchy to perform efficient display. This NSView is then embedded within a NSTableCellView that is displayed by a View-Based NSOutlineView.
The problem is that whenever I expand or collapse an item, all rows are being moved, but the layer's content remains displayed at the position it was before changing the outline.
Scrolling the NSOutlineView seems to refresh the layers and they resync with their rows at that point.
I have debugged this behavior using Instruments and it seems that the scrolling provokes a layout operation which updates the layers with a setPosition: call that should have occured when expanding or collapsing items.
Here is some sample code for a simple layer hosting NSView subclass.
#interface TestView : NSView
#end
#implementation TestView
- (instancetype)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
CAShapeLayer* layer = [CAShapeLayer layer];
layer.bounds = self.bounds;
layer.position = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds));
layer.path = [NSBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
layer.fillColor = [NSColor redColor].CGColor;
layer.delegate = self;
self.layer = layer;
self.wantsLayer = YES;
return self;
}
#end
I have tried a lot of potential solutions to this problem but I couldn't find any interesting method that gets called on the NSView instance that could be overriden to call [self.layer setNeedsDisplay] or [self.layer setNeedsLayout]. I also tried various setters on the CALayer itself such as :
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
layer.needsDisplayOnBoundsChange = YES;
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
Can anyone help me figure out how to make this layer display properly inside a NSOutlineView?
I ended up answering my question. The problem wasn't in the way my TestView was implemented. I simply missed one of the steps for enabling CoreAnimation support within the application. The relevant reference is within the Core Animation Programming Guide.
Basically, in iOS Core Animation and layer-backing is always enabled by default. On OS X, it has to be enabled this way :
Link against the QuartzCore framework
Enable layer support for one or more of your NSView objects by doing one of the following
In your nib files, use the View Effects inspector to enable layer support for your views. The inspector displays checkboxes for the selected view and its subviews. It is recommended that you enable layer support in the content view of your window whenever possible
For views you create programmatically, call the view’s setWantsLayer: method and pass a value of YES to indicate that the view should use layers.
Once I enable layer support on any of the NSOutlineView's parents, the various glitches are solved.
It is difficult to read the NSOutlineView reference documents and find the information about cell reuse that is likely giving you fits here.
You may have looked at outlineViewItemDidCollapse: but it's kind of a useless for our issue, because it doesn't have a pointer to an NSView, and that's because it's older than view-based outline views.
Perhaps the one helpful mention, buried within the NSOutlineViewDelegate protocol, down in the section on view-based NSOutlineView methods, there is a single mention within outlineView:didRemoveRowView:forRow: that:
The removed rowView may be reused by the table, so any additionally inserted views should be removed at this point.
In other words, when you call the outline view's makeViewWithIdentifier:owner:, for a cellView or rowView with a particular ID you often get a recycled view. Especially often because of collapse. Incidentally, that method is from the NSTableView superclass, and in that reference, there's also this comment:
This method may also return a reused view with the same identifier that is no longer available on screen. If a view with the specified identifier can’t be instantiated from the nib file or found in the reuse queue, this method returns nil.
So you have the option of altering the view hierarchy or niling properties in didRemoveRowView:forRow. However, buried within a third cocoa reference, that for NSView, there is within the commentary on prepareForReuse, this comment:
This method offers a way to reset a view to some initial state so that it can be reused. For example, the NSTableView class uses it to prepare views for reuse and thereby avoid the expense of creating new views as they scroll into view. If you implement a view-reuse system in your own code, you can call this method from your own code prior to reusing them.
So, TL;DR, you need to implement prepareForReuse.
The pertinent references are (mostly) the superclasses of both NSOutlineView and NSTableCellView.
And, FWIW, there was a similar question here, where the questioner seems to indicate things are even worse than I think, in that NSOutlineView is more creative behind the scenes than NSTableView.
In my own work with outline views and embedded NSTextViews, I've seen wildly terrible rendering hiccups relating to expand/collapse/scroll that I seem to have managed in just the NSOutlineViewDelegate methods. On iOS they did everyone the favor of renaming makeViewWithIdentifier to the more explicit dequeueReusableCellViewWithIdentifier.
You shouldn't have to enable layer backing for any of the ancestor views (like the outline view).
In my experience, the layer immediately assigned to a view (as opposed to sublayers) doesn't need its bounds, position, or autoresizing mask to be set. It is automatically made to track the bounds of the view. In fact, I would avoid setting those properties, just in case that breaks the automatic synchronization with the view's bounds rect.
So, the question is: how are you arranging for the view to move or resize with its superview? Are you using auto layout? If so, did you turn off its translatesAutoresizingMaskIntoConstraints? If yes to both, what constraints are you setting on the view? If no to either, how did you position the view within its superview? What frame did you set? Also, is the superview configured to autoresize its subviews (probably yes, since that's the default)? What is your view's autoresizingMask?
You could also override -setFrameOrigin: and -setFrameSize: in your custom view class and call through to super. Also, add logging to show when that's happening and what the new frame rect is. Is your view being moved as you expect when you expand or collapse rows?

iOS - hidden accessory becomes visible after orientation change

In my case I'm using a UITextField as the accessory that I don't need to show all the time. I've confirmed the change happens after the orientation notification events fire. I guess a hack would be to resize the accessory to zero height, but I'm reticent to do this.
Wondering if anyone has encountered this and found a solution?
Have entered a bug report and provided a sample project. For those with higher privileges, it is searchable on bugreport.apple.com as ID 16771757. I have also copied it to a Dropbox account accessible as https://www.dropbox.com/s/o28vo04ig3yhgz6/ID16771757.zip.
Thank you for reading.
iOS calls such methods for input accessory view instance:
[inputAccessoryView setAlpha:1]; when owner of accessory view becomes first responder (internal method call -[UIPeripheralHost(UIKitInternal) executeTransition:]);
[inputAccessoryView setHidden:NO]; when interface rotation finished (internal method call -[UIPeripheralHost finishRotationOfKeyboard:]);
That's why your input accessory view becomes visible after interface rotation event.
Solution depends on behaviour that you expect:
Let's imagine that input accessory view height = 44 ->
Now you hide input accessory view and set owner as first responder:
If you expect inputAccessoryView.frame.size.height equals 0 then solution for hiding input accessory view is set it to nil: inputAccessoryView = nil;
If you expect inputAccessoryView.frame.size.height equals 44 then solution for hiding input accessory view is override setHidden: method for it:
- (void)setHidden:(BOOL)hidden {
[super setHidden:self.customIsHiddenFlag];
}
where customIsHiddenFlag property that you need use for implementing logic of showing/hiding accessory view;
or override setAlpha: method:
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:(self.customIsHiddenFlag ? 0 : 1)];
}
These solutions work for iOS 7.
For iOS 6 you could use your base solution inputAccessoryView.hidden = YES and it works because iOS doesn't call setHidden:NO automatically when interface rotation is fired.
It seems that you are right and it's a bug cause of different behaviour on iOS6 and iOS7. If Apple wants to show input accessory view forcedly then they should call setHidden:NO not only after interface rotation but also when owner becomes first responder.
From Apple's documentation on inputAccessoryView:
… Subclasses that want to attach custom controls to either a system-supplied input view (such as the keyboard) or a custom input view (one you provide in the inputView property) should redeclare this property as readwrite and use it to manage their custom accessory view. [emphasis mine]
So the correct way to hide the accessory view would be redeclaring the property as readwrite:
#property (nonatomic, readwrite) UIView *inputAccessoryView;
removing the accessory view from superview and setting the property to nil when appropriate:
- (IBAction)hideAccessoryView:(UIButton *)sender
{
[self.inputAccessoryView removeFromSuperview];
self.inputAccessoryView = nil;
}
This is correct with regard to the docs but if you look at the view hierarchy, there's a UIPeripheralHostView (UIKit private class) that does not change its size. This most likely means that throwing out the accessory view will not be reflected by keyboard size — it'll stay the same. Keep this in mind if you plan to calculate any offsets to adjust to on-screen keyboard.
That said, the best way for you to move forward might be using a completely transparent view as the accessory view and have your custom view (UITextField in this case) as a subview. That way you will get both complete control over your custom view and consistent behaviour of your app on current and future versions of iOS.
Edit:
Here's a screenshot showing a slightly modified version of your bug report app with UIPeripheralHostView highlighted:
You can see how size of the view stays the same after the accessory view has been removed.
When you add an accessory view, you "pass" it to the system for layout. It is more than likely, when Apple performs layout on the keyboard view, it also layouts the accessory view and sets it to visible. Setting the accessory as hidden can also have other side effects, such as the keyboard height being incorrectly calculated, thus causing incorrect inset calculation.
From my experience, it is best to remove the accessory and add it again when necessary. Resizing the accessory view will cause other issues related to keyboard size. If you want to hide and show quickly, subclass the view that includes the accessory view, and implement internally the setting and removing of accessory view.

NSPopover padding content on one side

I have two NSPopovers, both of which are set up exactly the same (just linking a custom NSView from IB). Both pop up just fine, but one appears to offset the content by about 20px.
This NSPopover is (properly) not padding the content...
but this one adds about 20px from the ride side.
Here are the two views laid out in IB
As you can see, the search bar should be pinned to the right side like it is the left, but for some reason it is not. At first I thought it was a contraints issue, but after messing around with them for a while I can confirm it is not.
Any clue whats up?
EDIT: Decided to subclass the view and fill its rect, got some very strange results! The view appears to be offset. I have no clue why this is...
From here, this caught my eye (emphasis mine):
The principle circumstance in which you should not call
setTranslatesAutoresizingMaskIntoConstraints: is when you are not the
person who specifies a view’s relation to its container. For example,
an NSTableRowView instance is placed by NSTableView. It might do this
by allowing the autoresizing mask to be translated into constraints,
or it might not. This is a private implementation detail. Other views
on which you should not call
setTranslatesAutoresizingMaskIntoConstraints: include an
NSTableCellView, a subview of NSSplitView, an NSTabViewItem’s view, or
the content view of an NSPopover, NSWindow, or NSBox. For those
familiar with classic Cocoa layout: If you would not call
setAutoresizingMask: on a view in classic style, you should not call
setTranslatesAutoresizingMaskIntoConstraints: under autolayout.
Does it apply to you?
if you have an outlet to your content view, you should be able to force it into place with:
[contentView setFrame:[[contentView superview]bounds]];
or more modernly I guess...
contentView.frame = contentView.superview.bounds;
Proplem solved, but I still don't exactly know why it required solving. The NSPopovers content view (or at least mine) requires an origin point of X: 13, Y: 13. For some reason, only one of my views was getting this value.
To solve this, I subclassed NSView and overrode the setFrame method forcing its x and y to always be 13, 13
-(void)setFrame:(NSRect)frameRect {
[super setFrame:NSMakeRect(13, 13, frameRect.size.width, frameRect.size.height)];
}

Xcode's auto layout is only effective in viewDidAppear and this is very problematic

After upgrading my project to iOS 6, I realized that auto layout is only effective in viewDidAppear and most of my code expects the view's frame to be available in viewDidLoad. This limitation renders the really nice auto layout feature almost useless for me. Is there any suggestions to help me use auto layout?
For example, sometimes the developer needs to adjust information about a subview based on where auto layout chooses to place that particular subview. The subview's final location cannot be ascertained by the developer until AFTER the user has already seen it. The user should not see these information adjustments but be presented the final results all at once.
More specifically: What if I want to change an image in a view based on where auto-layout places that view? I cannot query that location and then change the image without the user seeing that happen.
As a general rule, the views frame/bounds should never be relied on in viewDidLoad.
The viewDidLoad method only gets called once the view has been created either programmatically or via a .nib/.xib file. At this point, the view has not been setup, only loaded into memory.
You should always do your view layout in either viewWillAppear or viewDidAppear as these methods are called once the view has been prepared for presentation.
As a test, if you simply NSLog(#"frame: %#", NSStringFromCGRect(self.view.frame)); in both your viewDidLoad and viewWillAppear methods, you will see that only the latter method returns the actual view size in relation to any other elements wrapped around your view (such as UINavigationBar and UITabBar).
As told by #charshep in a comment, calling view.layoutIfNeeded() in viewWillAppear can do the trick.
Quote of his original comment
I had trouble getting a table view to appear at the correct scroll position when pushing it [...] because layout wasn't occurring until after viewWillAppear. That meant the scroll calculation was occurring before the correct size was set so the result was off. What worked for me was calling layoutIfNeeded followed by the code to set the scroll position in viewWillAppear.

Accessing class that added scrollview as a sub view and some other questions

I'm working on an iOS app. Basically, one section of my app has a large scrollview with 6 other scrollviews added as sub views (using the method [scrollView addSubview:object.view]) so that it pages between 6 different scrollviews. Each scrollview has some textviews that I need to save the contents of.
In each sub scrollview, I added an inputAccessoryView to the keyboard to add a Done button, and I want to call a method to save the data when this button is pressed, and then remove the keyboard (the latter of which I already have done, I just need to figure out the saving part).
I feel that it would be easier to have one method in my 'super' scrollview that will save all of the data so I can just add in a few lines of code to each sub scrollview (by the way, these sub scrollviews aren't subclassed from the main scrollview, they are UIViewController subclasses, so I can't just use super) because I have to copy/paste it into 6 different files. I was able to access the 'super' class (the big scrollview that has all of the others in it) by importing it in the 'sub' scrollview and creating a new object, but I feel that's not what I'm supposed to do. That leads me into my next question.
If access the bigger scrollview in this way, trying to access the text property of the UITextViews returns null, whereas accessing the same property from within the class of the sub scrollview displays the text.
Please let me know if I'm doing something completely wrong, as I feel like having 6 different scrollviews is really inefficient (but testing it on two iPod touches, a 2nd gen and a 4th gen, there isn't much lag at all so it can't be too inefficient or else it'd hog memory and make it lag, right?)
Thanks,
Lee
EDIT:
I'm still having trouble with this. The button I created was created programmatically, so I can't link it to the first responder in Interface Builder. Is there a way to do this programmatically as well? Here's my code to create the inputAccessoryView:
-(void)createInputAccessoryView{
inputAccView = [[UIView alloc]initWithFrame:CGRectMake(10.0, 0.0, [[UIScreen mainScreen] bounds].size.width, 40.0)];
UIToolbar *toolbar = [[UIToolbar alloc]initWithFrame:CGRectMake(0.0, 0.0, 320.0, 40.0)];
UIBarButtonItem *doneBttn = [[UIBarButtonItem alloc]initWithTitle:#"Done" style:UIBarButtonItemStyleDone target:self action:#selector(endEdit:)];
[toolbar setItems:[NSArray arrayWithObject:doneBttn]];
[inputAccView addSubview:toolbar];
[toolbar release];
[doneBttn release];
}
The button points to a method (in the same class) that will close the keyboard. However, before I close the keyboard, I need it to call another method to save the data (which is in a different class). Does this help anyone's understanding?
When posting a question, show your code. Don't just describe your code. It's much easier for us to help you when we can see what you're talking about without the filter of what you think is important.
Nevertheless, I think I know what you are doing wrong and how to help you. You said this:
I was able to access the 'super' class (the big scrollview that has all of the others in it) by importing it in the 'sub' scrollview and creating a new object ... trying to access the text property of the UITextViews returns null
It sounds like you have put this in your child scrollview:
- (IBAction)saveButtonWasPressed:(id)sender {
ParentScrollView *parent = [[ParentScrollView alloc] init];
[parent saveEverything];
}
and then you expected this newly-created parent scroll view to be hooked up to your existing text views. But why would it be connected? When you load a view hierarchy from a nib, the nib loader takes care of making the connections that you established using Interface Builder. But if you create views in code, you have to establish the connections yourself, and you didn't.
You shouldn't be creating a new parent scroll view anyway. You need to send a message to the existing parent scroll view. There are a few ways you could do that. In this case I recommend using the responder chain.
You can read about the responder chain in Cocoa Application Competencies for iOS. Here are the important concepts for this problem:
When a text view is active, the text view is the first responder in the chain.
Each ancestor view of that text view is in the chain.
You can hook up a button to send an event up the responder chain when the button is touched:
In Interface Builder, you just connect the button to the "First Responder" placeholder (in the upper-left area of the window). You need to have already defined the IBAction in your source code for it to show up when you connect the button to the First Responder placeholder.
In code, you use nil as the target parameter of addTarget:action:forControlEvents:, like this:
[self.saveButton addTarget:nil action:#selector(saveEverything:) forControlEvents: UIControlEventTouchUpInside];
If you hook your button up that way, UIKit will take care of searching the responder chain for the first responder that responds to the saveEverything: message. In your case, that should be your (existing) parent scroll view.
One more thing: the parent scroll view isn't really the right place to put your save code. In the MVC design pattern, a controller is more appropriate. Lucky for you, if a view has a UIViewController associated with it, the UIViewController is also in the responder chain. So you can put your saveEverything: method on your view controller instead of your parent scroll view.