Scroll NSTableView to endOfDocument not the last row - macos

I have NSTableView with dynamic row heights. I need to scroll to the end. I tried scrollToEndOfDocument: but it gives same result as scrollRowToVisible: which is start of last row
- (void)viewDidLoad {
[super viewDidLoad];
//[[self tableView] scrollRowToVisible:[[self tableView] numberOfRows] - 1];
[[self tableView] scrollToEndOfDocument:self];
}

Your scrollRowToVisible approach should work. Here's a quick sample project that implements variable heights with a button that scrolls to the last row. That last row is fully visible after scrolling.
Update:
For table cells larger than the surrounding NSClipView, the above technique will only scroll to the top of the cell. To scroll to the bottom of the last cell, you can use:
let point = NSPoint(x: 0, y: tableView.frame.height)
tableView.scroll(point)
or since OP was in ObjC:
[[self tableView] scrollPoint: NSMakePoint(0, [self tableView].frame.size.height)]

Related

How to do batch display using NSTextView

I'd like to be able to show a view that resembles something like a console log, with multiple lines of text that are scrollable and selectable.
The fundamental procedure I have in mind is maintaining an array of strings (call it lines) and appending these to the textStorage of the NSTextView using a new line character as delimiter.
However there are a few factors to consider, such as:
Updating the textStorage on scroll so that it appears seamless to the user
Updating the textStorage on resizing the view height
Maintaining scroll position after the textStorage gets updated
Handling an out of memory possibility
Can someone please provide some guidance or a sample to get me started?
Add a string from your array to the NSTextStorage and animate the NSClipView bounds origin.
- (void)appendText:(NSString*)string {
// Add a newline, if you need to
string = [NSString stringWithFormat:#"%#\n", string];
// Find range
[self.textView.textStorage replaceCharactersInRange:NSMakeRange(self.textView.textStorage.string.length, 0) withString:string];
// Get clip view
NSClipView *clipView = self.textView.enclosingScrollView.contentView;
// Calculate the y position by subtracting
// clip view height from total document height
CGFloat scrollTo = self.textView.frame.size.height - clipView.frame.size.height;
// Animate bounds
[[clipView animator] setBoundsOrigin:NSMakePoint(0, scrollTo)];
}
If you have elasticity set in your NSTextView you need to monitor for its frame changes to get exact results. Add frameDidChange listener to your text view and animate in the handler:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Text view setup
[_textView setPostsFrameChangedNotifications:YES];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(scrollToBottom) name:NSViewFrameDidChangeNotification object:_textView];
}
- (void)appendText:(NSString*)string {
// Add a newline, if you need to
string = [NSString stringWithFormat:#"%#\n", string];
// Find range
[self.textView.textStorage replaceCharactersInRange:NSMakeRange(self.textView.textStorage.string.length, 0) withString:string];
}
- (void)scrollToBottom {
// Get the clip view and calculate y position
NSClipView *clipView = self.textView.enclosingScrollView.contentView;
// Y position for bottom is document's height - viewport height
CGFloat scrollTo = self.textView.frame.size.height - clipView.frame.size.height;
[[clipView animator] setBoundsOrigin:NSMakePoint(0, scrollTo)];
}
In real-life application you would probably need to set some sort of threshold to see if the user has scrolled away from the end more than the height of a line.

NSScrollView starting at middle of the documentView

I have the following code:
[[ticketsListScrollView documentView] setFrame: NSMakeRect(0, 0, [ticketsListScrollView frame].size.width, 53 * [tickets count])];
[[ticketsListScrollView documentView] setFlipped:YES];
for(int i = 0; i < [tickets count]; i++) {
TicketsListViewController *viewController = [[TicketsListViewController alloc] initWithNibName:#"TicketsListViewController" bundle:nil];
viewController.dateLabelText = tickets[i][#"date"];
viewController.timeLabelText = tickets[i][#"time"];
viewController.subjectLabelText = tickets[i][#"title"];
NSRect frame = [[viewController view] frame];
frame.origin.y = frame.size.height * i;
[viewController view].frame = frame;
[[ticketsListScrollView documentView] addSubview:[viewController view]];
}
if the list is large enough (many views), the NSScrollView starts at top-left, which is great. For less views (the views do not take the whole documentView, then NSScrollView starts at the middle.
Any idea why?
Thank you!
Views are not flipped by default, which means your document view is being pinned to the lower-left corner (the default, non-flipped view origin) of the scroll view. What you're seeing is a view not tall enough to push the "top" subview to the top of the scroll view. I see you tried flipping this view, so you already know about this, but you're not doing it correctly.
I'm not sure why you're not getting an error or a warning when calling -setFlipped: since the isFlipped property is read-only. In your document view (the view that's scrolled, and in which you're placing all those subviews), you can override it:
- (BOOL)isFlipped {
return YES;
}
Of course you'll have to put this in a custom NSView subclass and set that as your scroll view's document view's class in IB if you're not creating it at runtime. You'll also need to adjust the frames you use for layout, since you're currently expressing them in the coordinate system of the scroll view's frame. You should be expressing them in your container/layout view's bounds coordinates, which will also be flipped, and so, likely different from your scroll view's coordinates. You'll also need to implement -intrinsicContentSize (and call -invalidateIntrinsicContentSize when adding/removing subviews) so auto-layout can size the container appropriately.

Creating right aligned NSView

I want to create a NSView container such that any NSControl object added should be right aligned.
I have added a method to MyCustomNSView class as following. Currently I am adding buttons which are getting left aligned.
- (void) _addButton:(NSString *)title withIdentifier:(NSString *)identifier {
NSButton *button = [[NSButton alloc] initWithFrame:NSMakeRect(100 * [_buttonIdentifierList count] + 10 , 5, 70, 20)];
[button setTitle:title];
[button setAction:#selector(actionButtonPressed:)];
[button setTarget:self];
[button setIdentifier:identifier];
[self addSubview:button];
[_buttonIdentifierList addObject:identifier];
}
So what modifications do I have to make to the above method so that it will add the objects from right side.
I was planning to do it mathematically(Generating frame origin that would generate right aligned origin point). I also tried out using NSLayoutConstrains but didnt work out..
How do I do it using autolayouts ?
To do it by manual positioning, you would compute the frame for the button something like this:
NSButton *button = [[NSButton alloc] initWithFrame:NSMakeRect(NSMaxX(self.bounds) - (100 * [_buttonIdentifierList count] + 10) - 70, 5, 70, 20)];
That is, you take your current calculation which is an offset toward the right (from the left edge) and negate it to make it an offset toward the left. You add the value of the right edge of the containing view so it's an offset from the right edge. That has computed the X position of the right edge of the button, so you subtract the button's width to get the origin of the button, which is on its left edge.
To use auto layout (which uses NSLayoutConstraint), you could do this:
NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect];
[button setTitle:title];
[button setAction:#selector(actionButtonPressed:)];
[button setTarget:self];
[button setIdentifier:identifier];
button.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:button];
__block NSButton* previousButton = nil;
if (_buttonIdentifierList.count)
{
NSString* previousButtonIdentifier = _buttonIdentifierList.lastObject;
[self.subviews enumerateObjectsUsingBlock:^(NSView* subview, NSUInteger idx, BOOL *stop){
if ([subview.identifier isEqualToString:previousButtonIdentifier])
{
previousButton = (NSButton*)subview;
*stop = YES;
}
}];
}
NSDictionary* metrics = #{ #"buttonWidth": #70,
#"buttonHeight": #20,
#"buttonSeparation": #30,
#"horizontalMargin": #10,
#"verticalMargin": #5 };
if (previousButton)
{
NSDictionary* views = NSDictionaryOfVariableBindings(button, previousButton);
NSArray* constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[button(buttonWidth)]-(buttonSeparation)-[previousButton]" options:NSLayoutFormatAlignAllBaseline metrics:metrics views:views];
[self addConstraints:constraints];
}
else
{
NSDictionary* views = NSDictionaryOfVariableBindings(button);
NSArray* constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[button(buttonWidth)]-(horizontalMargin)-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[button(buttonHeight)]-(verticalMargin)-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
}
[_buttonIdentifierList addObject:identifier];
Finding the previousButton would be simplified if you keep track of the buttons, rather than the identifiers. If you have a button object, it's easy to get its identifier, but the reverse (getting the button object when all you have is the identifier) is not as simple.
If you want to allow the buttons to be their natural width and height, rather than a fixed value, you can just leave out those width/height specifiers (that is, use [button] rather than [button(buttonWidth)]). If you want all of the buttons to have the same width, but let the system pick the width of the naturally widest button, you can use [button(==previousButton)]. Since a button's default compression resistance priority is higher than its content hugging priority, it will pick the smallest width that doesn't compress any of them.
If you want the buttons to be the standard distance away from each other, rather than the fixed value of 30 points, you can use use - instead of -(buttonSeparation)-. Similarly, if you want them to be the standard distance from the superview edge, you can use - instead of -(horizontalMargin)- or -(verticalMargin)-.

UIScrollview+autolayout seems not working in iOS8/Xcode 6 preview?

The following steps for UIScrollView+autolayout has been working for me, but not in the iOS8/Xcode 6 preview: (using storyboard, size class enabled):
add a scrollview to the root view.
pin zero spaces to all edges of super view.
add a UIView (contentView) to the above scrollview.
pin zero spaces to all edges of the scrollview
add some widgets to contentView and change the height of the contentView to 2000.
=> this contentView scrolls in iOS 7, but I cannot get the same steps working in iOS 8 preview.
Even it seems working in iOS 7, it is possible that I may not doing the right way? Any suggestions?
I'm surprised not to have seen more comment about this. Scroll view internal autolayout is largely broken in iOS 8 (as seeded up to the time of this writing).
EDIT This was fixed in seed 5, so this note should be ignored!
The rule is supposed to be (see https://developer.apple.com/library/prerelease/ios/technotes/tn2154/_index.html) that if the content of a scroll view (its subview or subviews) is pinned to all four bounds of the scroll view, it sets the content size.
In iOS 8, however, this fails - but in an odd way. It fails only if the constraints determining the height and width of the subviews are all absolute as opposed to intrinsic.
Thus, for example, consider the code at the bottom of that tech note, where a scroll view and a really big image view are created in code (here it is; I have corrected a small typo where an at-sign was dropped):
- (void)viewDidLoad {
UIScrollView *scrollView;
UIImageView *imageView;
NSDictionary *viewsDictionary;
// Create the scroll view and the image view.
scrollView = [[UIScrollView alloc] init];
imageView = [[UIImageView alloc] init];
// Add an image to the image view.
[imageView setImage:[UIImage imageNamed:#"MyReallyBigImage"]];
// Add the scroll view to our view.
[self.view addSubview:scrollView];
// Add the image view to the scroll view.
[scrollView addSubview:imageView];
// Set the translatesAutoresizingMaskIntoConstraints to NO so that the views
// autoresizing mask is not translated into auto layout constraints.
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.translatesAutoresizingMaskIntoConstraints = NO;
// Set the constraints for the scroll view and the image view.
viewsDictionary = NSDictionaryOfVariableBindings(scrollView, imageView);
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[scrollView]|"
options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[scrollView]|"
options:0 metrics: 0 views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[imageView]|"
options:0 metrics: 0 views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[imageView]|"
options:0 metrics: 0 views:viewsDictionary]];
}
That code works (assuming you have a really big image), because the image view is sized by intrinsic constraints. But now change the last two lines, like this:
[scrollView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[imageView(1000)]|"
options:0 metrics: 0 views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[imageView(1000)]|"
options:0 metrics: 0 views:viewsDictionary]];
Now what you have is a scroll view that is scrollable on iOS 7 but is NOT scrollable on iOS 8. Further investigation shows that this is because the content size remains at (0,0); it does not respect the absolute width and height constraints of the content view.
Use following step for UIScrollView + AutoLayout
Add scroll view to the root view
Add contain view to above scroll view
Add Following constraint for scroll view
Trailing space to super view = 0
Leading Space to super view =0
Top space to super view = 0
Bottom Space to super view = 0
Add Following Constraint for contain view of scroll view
(in this case scroll view is super view)
Trailing space to super view = 0
Leading Space to super view =0
Top space to super view = 0
Bottom Space to super view = 0
Height of contain view (if you are using vertical scrolling) otherwise width of contain view (if you are using Horizontal scrolling).
Horizontal canter alignment (if you are using vertical scrolling) otherwise vertical canter alignment (if you are using Horizontal scrolling).

View-backed NSTableView: Inserted Column Width/Margin Issues

I have a view-based NSTableView that usually has one column in it. However, at a button press, I want a new column to slide in from the left (very similar to what happens on an iPhone when you click the Edit button in Mail). For now, the view that I want to slide is in a very simple view that draws a solid background: its drawRect: just does
[[NSColor blueColor] set];
[[NSBezierPath bezierPathWithRect:[self bounds]] fill];
In the delegate for my NSTableView, I have the following:
- (IBAction)buttonPressed:(id)sender
{
NSTableColumn *newColumn = [[NSTableColumn alloc] initWithIdentifier:#"InPreviewColumn"];
[newColumn setWidth:40];
[newColumn setMinWidth:[newColumn width]];
/* To make up for there not being an insertColumnAt: method,
hide the column, add it, and move it to the front before showing it. */
[newColumn setHidden:YES];
[availableFontsView beginUpdates];
[availableFontsView addTableColumn:newColumn];
[availableFontsView moveColumn:1 toColumn:0];
[availableFontsView endUpdates];
[newColumn setHidden:NO];
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
/* ... code for main column ... */
else if([[tableColumn identifier] isEqualToString:#"InPreviewColumn"])
{
USSolidBackgroundView *v = [tableView makeViewWithIdentifier:[tableColumn identifier] owner:self];
if(!v)
{
v = [[[USSolidBackgroundView alloc] initWithFrame:NSMakeRect(0, 0, [tableColumn width], 0)] autorelease];
[v setIdentifier:[tableColumn identifier]];
[v setAutoresizingMask:NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin];
}
return v;
}
}
Yet, when I do this, I end up with this result (there's a big non-blue margin between the blue part of the new column and the start of the main column):
When the additional column isn't present, there's no margin so I'm pretty sure that the problem isn't with the other view (since there's no margin when it's the only view displayed).
I've used logging statements to verify that solid color view's bounds always has a width of 40 (in its drawRect:) and, at least when the view is created, the table column has a width of 40 as well.
So where does this margin come from? No matter how I size the column, it seem that only roughly half of it is blue. So, the bigger the column, the bigger the margin.
How do I make the entire extra column blue?
The issue was my being stupid: In the view that draws the font names, I was basing the position on [self frame]. Which is wrong.
[self bounds] is the way to go. Which I knew. Can't believe I made this mistake. Amateur hour.
If you wander by this question, feel free to vote to close it or flag it or whatever it is that's supposed to work on Stackoverflow.
My apologies.

Resources