How to set up a NSLayoutConstraint in a NSScrollView? - macos

I am new to NSLayoutConstraint.
I want to manage the content of a NSScrollView programmatically. At the end, what I want to do is to create a subclass of NSScrollView with a method - add:(NSView *)aView that will add aView after the others and enlarge the documentView of the NSScrollView if necessary.
(all this for OSX) I guess it exists but can't find it, so if anyone knows where to find this, I am in ;) Here is a picture that can help you to understand :
My plan to do this is to use the NSLayoutConstraints. The constraints will be :
equal vertical space between view 1, view 2, etc.
the documentView height should adapt to the sum of the heights of view 1, view 2, etc.
some fixed vertical margins between the documentViewand view 1 and the last view.
view 1, view 2, etc. are right-aligned
So, let us come to my question : I have started by just adding view 1 (I will see it after for the rest). I did the following :
theScrollView is my NSScrollView ;
newSubview is the view I want to add ; with respect to the picture, it is view 1.
theContainer = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)] ;
[theScrollView setDocumentView:theContainer] ;
[newSubview setTranslatesAutoresizingMaskIntoConstraints:NO];
[theContainer addSubview:newSubview] ;
NSLog(#"superview ? %#", [newSubview superview]) ;
NSLog(#"view ? %#", newSubview) ;
NSLog(#"NSScrollView ? %#", self) ;
NSLog(#"NSScrollView content ? %#", self.contentView) ;
NSArray * arrayOfConst1 ;
arrayOfConst1 = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-50-[newSubview]"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(newSubview)];
NSLog(#"First constraints : %#", arrayOfConst1) ;
NSArray * arrayOfConst2 ;
arrayOfConst2 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[newSubview]"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(newSubview)];
NSLog(#"Second constraints : %#", arrayOfConst2) ;
[newSubview removeConstraints:[newSubview constraints]] ;
[theContainer removeConstraints:[theContainer constraints]] ;
[theContainer addConstraints:arrayOfConst1] ;
[theContainer addConstraints:arrayOfConst2] ;
NSLog(#"Frame of theContainer : %#", NSStringFromRect(theContainer.frame));
But, it doesn't work, the frame of theContainer remains (0,0,0,0).
I am new to NSLayoutConstraints ; what I am missing ?

Related

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)-.

What is the best way to display a single-paged pdf as an image?

I would like to display in an NSView a single-paged PDF.
So far, I have two solutions but they both have downsides. Can anyone help me with any of these downsides?
First solution: with NSImage and NSImageView
NSString *path= [[NSBundle mainBundle] pathForResource:name ofType:#"pdf"];
NSImage * image = [[NSImage alloc] initWithContentsOfFile:path] ;
NSImageView * imageView = [[NSImageView alloc] init] ;
imageView.frame = NSMakeRect(0, 0, 2*image.size.width, 2*image.size.height) ;
imageView.image = image ;
imageView.imageScaling = NSImageScaleAxesIndependently ;
return imageView
Downsides:
the image is not anti-aliased
I don't understand why the factor 2 is needed. Why does my PDF is displayed smaller in an NSView than it is with the Finder?
Second solution: with PDFDocument and PDFView
NSString *path= [[NSBundle mainBundle] pathForResource:name ofType:#"pdf"];
NSURL *urlPDF = [NSURL fileURLWithPath:path] ;
PDFDocument * myPDFDocument = [[PDFDocument alloc] initWithURL:urlPDF] ;
PDFView *myPDFView = [[PDFView alloc] init] ;
myPDFView.document = myPDFDocument ;
PDFPage * firstPage = [myPDFDocument pageAtIndex:0] ;
NSRect myBounds = [firstPage boundsForBox:kPDFDisplayBoxMediaBox] ;
NSRect myNewBounds = NSMakeRect(0, 0, myBounds.size.width*2, myBounds.size.height*2+5) ;
myPDFView.frame = myNewBounds ;
myPDFView.autoScales = YES ;
return myPDFView ;
Downsides:
I am able to select the text of my pdf, I can zoom in or zoom out. But I would like my PDF document to be displayed as an image, without these possibilities
I don't understand why the factor 2 is needed. Why is my PDF displayed smaller in an NSView than it is with the Finder?
There are some margins around my image
I'm not seeing the problems you describe with NSImageView. I implemented a nib-based window and NSImageView. In my case I have an overlapping sibling view, so I turned CALayers turned on in the nib. I'm on 10.9.2. Sizing is normal (1x) and the text in my PDF is anti-aliased (sub-pixel I think, since I see colors when I blow it up). I do have scaling NONE - maybe scaling is preventing anti-aliased text?
Otherwise my guess is there's something different about your views or or PDF content. Try a simpler PDF and/or a nib-based view and if it works, you can look for differences.

NSTextField width and autolayout

I am trying to create an NSTextField programmatically.
I want to use this NSTextField with auto layout, so its width will be defined automatically to display the entire line (there is only one line of text).
The problem is that textField.intrinsicContentSize and textField.fittingSize are both have -1 and 0 values for the horizontal coordinate, as the output of the code below is:
textField.intrinsicContentSize={-1, 21}
textField.fittingSize={0, 21}
The code:
NSTextField* textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 10, 24)];
NSString* str = #"One line of text here.";
[textField setStringValue: str];
[textField setTranslatesAutoresizingMaskIntoConstraints:NO];
[textField invalidateIntrinsicContentSize];
NSLog(#"textField.intrinsicContentSize=%#", NSStringFromSize(textField.intrinsicContentSize));
NSLog(#"textField.fittingSize=%#", NSStringFromSize(textField.fittingSize));
This makes the text field to have a zero width assigned by auto layout.
What should I do to get meaningful values for the fittingSize and intrinsicContentSize properties of the text field so they reflect the content of the text field?
Another way to compute the text field optimal width without using fittingSize is using the following code (replacing John Sauer's sizeTextFieldWidthToFit method):
- (void) sizeTextFieldWidthToFit {
[textField sizeToFit];
CGFloat newWidth = NSWidth(textField.frame);
textFieldWidthConstraint.constant = newWidth;
}
You can set the priority of textFieldWidthConstraint to be lower than the priority of another inequality constrain the represent your requirement to a minimal size of 25.
I can't explain why NSTextField's intrinsicContentSize's widths are -1, but I was able to calculate one based off its attributedString's size. Here's how I created the NSTextField:
// textField is an instance variable.
textField = [NSTextField new] ;
textField.translatesAutoresizingMaskIntoConstraints = NO ;
[view addSubview:textField] ;
NSDictionary* viewsDict = NSDictionaryOfVariableBindings(view, textField) ;
// textFieldWidthConstraint is an instance variable.
textFieldWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[textField(==0)]" options:0 metrics:nil views:viewsDict] [0] ;
[view addConstraint:textFieldWidthConstraint] ;
NSNumber* intrinsicHeight = #( textField.intrinsicContentSize.height ) ;
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[textField(==intrinsicHeight)]" options:0 metrics:NSDictionaryOfVariableBindings(intrinsicHeight) views:viewsDict]] ;
// Position textField however you like.
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[textField]" options:0 metrics:nil views:viewsDict]] ;
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[textField]" options:0 metrics:nil views:viewsDict]] ;
[self sizeTextFieldWidthToFit] ;
// set textField's delegate so that sizeTextFieldWidthToFit is called when textField's text changes.
textField.delegate = self ;
And here are the methods that resize it as its text changes:
- (void) controlTextDidChange:(NSNotification*)aNotification {
if ( aNotification.object == textField )
[self sizeTextFieldWidthToFit] ;
}
- (void) sizeTextFieldWidthToFit {
// I'd like the width to always be >= 25.
CGFloat newWidth = 25 ;
if ( ! [textField.stringValue isEqualToString:#""] ) {
NSDictionary* attributes = [textField.attributedStringValue attributesAtIndex:0 effectiveRange:nil] ;
NSSize size = [textField.stringValue sizeWithAttributes:attributes] ;
newWidth = MAX(newWidth, size.width + 10) ;
}
textFieldWidthConstraint.constant = newWidth ;
}
I'm adding this here because these answers are old and I couldn't find everything needed to get NSTextField to automatically wrap and play nice using autolayout and NSConstraints in a single answer.
With these settings the text will:
Remain in the horizontal boundaries set by the constraints.
Wrap automatically on word boundaries.
Automatically adjust its height as required.
Push down anything below (if attached by constraints).
This is the code to make it just work:
NSTextField *instructions = [NSTextField labelWithString:#"Some very long text..."];
instructions.translatesAutoresizingMaskIntoConstraints = NO;
instructions.autoresizesSubviews = YES;
instructions.usesSingleLineMode = NO;
instructions.lineBreakMode = NSLineBreakByWordWrapping;
[instructions cell].wraps = YES;
[instructions cell].scrollable = NO;
instructions.maximumNumberOfLines = 10;
instructions.preferredMaxLayoutWidth = 400.0;
[self addSubview:iCloudInstructions];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[instructions]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(instructions)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[instructions]-(20)-[button]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(instructions, instructions)]];

What constant can I use for the default Aqua space in Autolayout?

According to the Cocoa Auto Layout Guide, I can use a dash in the visual constraint format language to "denote the standard Aqua space:"
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"[a]-[b]"
options:0
metrics:nil
views:viewDict]];
However, I can't seem to find an NSLayout... constant or method that allows me to do the same thing if I'm building a constraint without using the visual format language:
[self addConstraint:[NSLayoutConstraint constraintWithItem:a
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:b
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:<# ??? #>]];
Is there a constant (or another value or method) that I can use to define the Aqua space in such a situation?
I've found the "standard Aqua space" to be 8.0 between sibling views, and 20.0 between a view and its superview.
NSView* view = [NSView new] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSiblings = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-[view]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
CGFloat standardConstantBetweenSiblings = constraintWithStandardConstantBetweenSiblings.constant ; // 8.0
NSView* superview = [NSView new] ;
[superview addSubview:view] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSuperview = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
CGFloat standardConstantBetweenSuperview = constraintWithStandardConstantBetweenSuperview.constant ; // 20.0
For iOS users:
#import "NSLayoutConstraint+StandardOffsets.h"
#implementation NSLayoutConstraint (StandardOffsets)
+ (CGFloat)standardConstantBetweenSiblings
{
static CGFloat value;
if(!isnormal(value)) {
UIView *view = [UIView new] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSiblings = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-[view]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
value = constraintWithStandardConstantBetweenSiblings.constant ; // 8.0
}
return value;
}
+ (CGFloat)standardConstantBetweenSuperview
{
static CGFloat value;
if(!isnormal(value)) {
UIView *view = [UIView new] ;
UIView *superview = [UIView new] ;
[superview addSubview:view] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSuperview = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
value = constraintWithStandardConstantBetweenSuperview.constant ; // 20.0
}
return value;
}
#end
PS: I entered a bugreport that no constant is offered in the header files!
Based on John Sauer's answer, I wound up writing a couple methods on an NSLayoutConstraint category to find the constants:
+standardAquaSpaceConstraintFromItem:toItem: returns a single NSLayoutConstraint constructed using the visual format language; it asserts that the array of constraints generated from the format has exactly one item, then gets that item and gives it back.
+standardAquaSpaceFromItem:toItem: pulls the constant out of the constraint from the previous method and returns it as a CGFloat.
This way, I can either get the constant value myself if I need to do computations, or just get a single layout constraint with the right spacing (e.g. for assigning to a constraint #property or directly adding to my view).

removing/adding CALayers for GPU optimization

I have a layer backed view, I am trying to add subLayers roughly sized around 300 X 270 (in pixels) to it.
The sublayers' count may reach 1000 to 2000, not to mention each sublayer is again scalable to roughly 4280 X 1500 or more for starters.
So the problem is obviously that of a GPU constraint.
After adding around 100 subLayers sized 300 X 270 , there is a warning image is too large for GPU, ignoring and that is messing with the layer display.
The solution for such a problem (from some mailing lists) was to use CATiledLayer, but I can't make use of the tiledLayer due to the complex requirement of the subLayers' display.
Is there a possibility of removing the subLayers which don't fall under VisibleRect of the view?
I tried to removeFromSuperlayer and then add it whenever required, there's always a crash when I try to add the subLayer back.
How can I do this?
I am adding sublayer twice (I need to change it) but for now just for the gist of the code:
-(IBAction)addLayer:(id)sender
{
Layer *l = [[Layer alloc] init];
CALayer *layer = [l page];
[contentArray addObject:page];
[drawLayer addSublayer:layer];
[self layout];
}
-(void)layout
{
NSEnumerator *pageEnumr = [contentArray objectEnumerator];
float widthMargin = [self frame].size.width;
CGRect rect;
float zoom = [self zoomFactor];
while(obj = [contentEnmr nextObject] )
{
[obj setZoomFactor:zoom];
CALayer *pg =(CALayer *)[obj page] ;
rect = pg.bounds;
if ( x + pg.bounds.size.width > widthMargin )
{
x = xOffset;
y += rect.size.height + spacing ;
}
rect.origin = CGPointMake(x,y);
[obj changeBounds];
NSRect VisibleRect = [self visibleRect];
NSRect result = NSIntersectionRect(VisibleRect,NSRectFromCGRect( rect));
if( NSEqualRects (result ,NSZeroRect) )
{
[pg removeFromSuperlayer];
}else
{
[drawLayer addSublayer:pg];
[pg setFrame:rect];
[pg setNeedsDisplay];
}
x += ( rect.size.width + spacing);
}
NSRect viewRect = [self frame];
if(viewRect.size.height < ( y + rect.size.height + spacing ) )
viewRect.size.height = ( y + rect.size.height + spacing) ;
[self setFrameSize: viewRect.size];
}
#interface Layer : NSObject {
CALayer *page;
}
#property (retain) CALayer *page;
Have a look at the PhotoScroller application included as part of the WWDC conference. It demonstrates how to zoom and scroll through a very large image by loading only portions of that image that are currently visible.
Also check out this discussion.
You'll need to do what NSTableView and UITableView do, and manage the addition / removal of layers yourself whenever the visible rect changes. Subscribe to the boundsDidChange noitification of the enclosing scroll view's clip view (I'm assuming that the reason some of the layer is offscreen is that it's enclosed in a scroll view):
- (void) viewDidMoveToSuperview
{
NSClipView* clipView = [[self enclosingScrollView] contentView];
[clipView setPostsBoundsChangedNotifications:YES];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(clipViewBoundsDidChange:)
name:NSViewBoundsDidChangeNotification
object:clipView];
}
and then write a clipViewBoundsDidChange: method that adds and removes sublayers as needed. You may also want to cache and reuse invalidated layers to cut down on allocations. Take a look at the way UITableView and NSTableView interact with their dataSource object for some ideas about how to design the interface for this.
CATiledLayer solves this problem the content of a layer --- ie, whatever you set its contents property or draw into its graphics context directly. It won't do this for sublayers, in fact I think you're advised not to add sublayers to a CATiledLayer at all, as this interferes with its drawing behaviour.

Resources