Embedding a field editor in a NSScrollView - objective-c

Has anyone ever had experience embedding a field editor (for a NSTextField) inside a scroll view? I'm trying to make the NSTextField scrollable while editing.
Things I've tried:
Dynamically embed it when the custom field editor's -becomeFirstResponder gets called. This semi works; the problem is that when the NSTextField gets resized during editing the custom field editor no longer gets resized with it (and I need this - making an accordion
style application)
Create a "masquerading" field editor out of a NSScrollView, and using NSInvocation forward the methods to the actual surrogate field editor. This is the method I really hope would work; I've implemented all the methods as listed here; but I get an EXC_BAD_ACCESS whenever the field editor is actually loaded (e.g. when I call [customTextField selectText:nil]). I can't seem to pry any information out of the debugger even with Zombies enabled, and looking at the logs of NSObjCMessageLoggingEnabled yields nothing either. It seems like these guys got it working but that was seven years ago.
The last resort would be to drop NSTextFields completely and use NSTextViews (or instead of relying on the field editor mechanism, write one myself), but since I have many rows of data of which only one will be edited at a time, I don't want to instantiate a NSTextView for every single one of them... but then, perhaps it won't be so bad.

I ended up using option 1, and getting it to work without much difficulty. Option 2 was a complete dead end because EXC_BAD_ACCESS popped up everywhere I went.
My custom field editor now keeps a reference to a (custom) scroll view to embed itself in (vvScrollView), and inserts it into the view hierarchy. My code inside my custom field editor (NSTextView) for embedding it inside a scroll view, which is called as soon as the field editor becomes first responder and is automatically inserted into the view hierarchy:
- (void)embedSelfInScrollView {
NSView *realSuperview = [[self superview] superview];
// [self superview] is some kind of private NSClipView class
if ([realSuperview isKindOfClass:[NSTextField class]]) { // the expected behavior: this may change? TODO make less prone to chance
[realSuperview addSubview:[self vvScrollView]]; // insert into view
[[self vvScrollView] setFrameSize:[realSuperview frame].size]; // se the initial size equivalent to control size so it can autoresize the same way
// add the scrollview into the view hierarchy
[[self vvScrollView] setDocumentView:self]; // removes self from previous superview
}
}
The initial problem I had was that I was trying to insert the scrollview into the superview immediately above the field editor's (the private class of NSClipView) which broke almost every automatic sizing option (because I want to be able to resize the NSTextField while editing). Going a step further and bypassing the private class seems to work, but almost seems arbitrary.

Related

NSTextView scroll adjustment when typing at end of document

I have a NSTextView with a pretty big bottom indentation. I'm using textDidChange delegate method to adjust some attributes in the text whenever it is changed.
When typing at the edge of the screen and the end of the document, the scroll correctly gets adjusted at the bottom. However, when typing in another character after that, the scroll jumps up a bit.
Looking at the call stack, I see different behavior between typing at a position which will cause the line to wrap, as compared to a normal line.
When typing causes wrapping, the call stack looks like this:
[NSTextView adjustScroll:]
[NSClipView _scrollTo:animateScroll:flashScrollerKnobs:]
[NSTextView keyDown:]
Normally, textDidChange: is called between keyDown: and _scrollTo:, but not here.
However, when typing on the line which doesn't cause a wrap, textDidChange is called as usual:
[NSTextView adjustScroll:]
[NSClipView _scrollTo:animateScroll:flashScrollerKnobs:]
// some attribute changes here
[Document textDidChange:]
[NSTextView keyDown:]
There are multiple questions on SO which state that changing attributes on NSTextView causes all sorts of scrolling trouble, but it's surprising that the scroll values are completely off between these two events. I'd rather have two equal results, but I'm confused what to do here. _scrollTo is a private method and can't be overridden, and I can't really override keyDown either.
Is there a way to hook into the call order of scrolling and the textDidChange event, or to change the control flow of text and clip views?
Further investigation
To reproduce the issue, you'll have to meet at least these conditions:
• You are setting attributes in delegate method
• Text view is scaled (using scaleUnitSquareToSize)
• You might need to make changes to both temporary attributes and the text view's attributed string
• Non-contiguous layout is on.
Based on this thread from 2016 it seems to be a non-contiguous layout bug, which is yet to be fixed. A blog post from 2017 goes through similar issues regarding the order in which text and attributes are processed.

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.

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

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.

How to setup a NSTableView with a custom cell using a subview

I am trying to setup a NSTableView with a custom cell using an ArrayController and Bindings. To accomplish this I added a subview to the custom cell. The data connection seems to work somewhat. Though, there seems to be a redraw problem which I cannot fix. When I load the application only some of the cells are rendered. When I scroll through the rows or select one the rendering changes.
I created an example project on github to illustrate what the problem is.
The actual source code for the cell rendering can be found here:
// CustomCell.m
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
if (![m_view superview]) {
[controlView addSubview:m_view];
}
// The array controller only gets wrapped data items pack by the NSObjectTransformer.
// Therefore, objectValue returns a NSObjectWrapper.
// Unpack the wrapper to retreive the data item.
DataItem* dataItem = [(NSObjectWrapper*)[self objectValue] original];
[[m_view name] setStringValue:dataItem.name];
[[m_view occupation] setStringValue:dataItem.occupation];
[m_view setFrame:cellFrame];
}
It seems as if the parent controlView does not redraw. Can I force it somehow?
This is almost certainly not a best practice way of doing this, and I'll explain why afterwards: however, it does seem to work. Replace your cell class's drawInteriorWithFrame:inView: method with the following:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
DataItem* dataItem = [(NSObjectWrapper*)[self objectValue] original];
[[m_view name] setStringValue:dataItem.name];
[[m_view occupation] setStringValue:dataItem.occupation];
[m_view setFrame:cellFrame];
NSData *d = [m_view dataWithPDFInsideRect:[m_view bounds]];
NSImage *i = [[NSImage alloc] initWithData:d];
[i setFlipped:YES];
[i drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
The problem is that only one NSCell is created for the entire table. That's how cells are meant to work: the table view creates a cell, and calls setObject… followed by drawInterior… over and over again to get the cell to draw the whole table. That's great from an efficiency perspective (the NSCell class was designed back when 25mhz was a fast computer, so it aimed to minimise the number of object allocations), but causes problems here.
In your code, you populate a view with values, and set its frame, adding it as a subview of the table view if needed. However, since you've only got one instance of NSCell, there can only be one view: you took the single view that you had and merely moved it down the rows of the table.
To do this properly, you'd need some data structure to track all the views you added as subviews of your NSTableView, and when the cell is updating one in the drawInterior… method you'd need to look up which the correct one was and update that. You'd also need to allocate all these views in code (or at least move the view to a separate nib which you could load multiple copies of), because as it is you've only got one in your nib and copying a view is a pain.
The code I wrote is a kludge, since it's really inefficient. What I did was each time the view needs to draw, I drew the view into an off screen image buffer, and then drew the buffer into the correct place in the table view. In doing so, I avoided the problem of only having one view, since the code just takes and draws a new copy of its contents whenever it is needed.
EDIT: See my other answer for explanation
Have you implemented copyWithZone:? You'll need to ensure you either copy or recreate your view in that method, otherwise different cells will end up sharing a view (because NSTableView copies its cells).

Is there a way to have varying views in an NSCollectionView?

I am wanting something similar to how iWork has the template selection screen for Pages when you can select different templates, and each view contains different info has difference sizes etc.
I have tried subclassing NSCollectionView and determining which view to display using the newItemForRepresentedObject method (as opposed to using itemPrototype view Interface Builder), but it for some reason doesn't position the views correctly, and it does not show the correct number of views for the number of items present. Here is my code. I was hoping someone may have a better way to do this, or an example of how this is done.
personView and companyView are properties in the subclassed NSCollectionView, that are IBOutlets to views in IB.
-(NSCollectionViewItem *)newItemForRepresentedObject:(id)object{
NSCollectionViewItem *collectionViewItem = [[NSCollectionViewItem alloc] init];
[collectionViewItem setRepresentedObject:object];
if([[object valueForKey:#"company"] boolValue] == YES){
NSView *view = [companyView retain];
[collectionViewItem setView:companyView];
}else{
[collectionViewItem setView:personalView];
}
return collectionViewItem;
}
(It doesn't even seem possible to make an NSCollectionView with differently-sized item views; each size would need to be a multiple or integer divisor of some "main" size, and you'd need to do massive item-checking and -reordering to be sure it's even possible to render them in a grid. Are you sure you're asking the right question?)
Also, I don't see anything like this in iWork: all the views in its template chooser are the same. (Though their NSImageView subviews are of different sizes.) I'd recommend if at all possible using the same view and changing its subviews appropriately. It's easy to, for example, bind text fields' "hidden" property or change the width of an image view. Can't you make a single view that works for both classes, changing itself appropriately depending on the represented object?