Centering UIImage and UILabel together - uiimage

I want to center my label's text while also considering a 20x20 image that should always be just to the left of the label. Specifically, I have three labels who's text could be anything, and I want a check mark to always appear just to the left of each label - this position will vary depending on the text length though.
My best guess is making the image a subview of the label and then indenting the text but that still will be somewhat inconsistent...

In iOS 6 you can do this with constraints and almost no code. But for iOS 5 using only springs and struts you need to manage the view frames your self. You will also need to call sizeToFit so the UILabel resizes it's self to the current text.
Here is some example code:
- (void)centerLabels
{
CGRect viewFrame = self.view.frame;
NSArray *allLabelsAndChecks = #[ #[self.label1, self.check1], #[self.label2, self.check2], #[self.label3, self.check3] ];
for (NSArray *labelAndCheck in allLabelsAndChecks) {
UILabel *label = labelAndCheck[0];
UIImageView *check = labelAndCheck[1];
[label sizeToFit];
CGRect labelFrame = label.frame;
CGRect checkFrame = check.frame;
CGFloat maxWidth = viewFrame.size.width - (leftMarginText + rightMargin);
if (labelFrame.size.width > maxWidth) {
labelFrame.origin.x = leftMarginText;
labelFrame.size.width = maxWidth;
checkFrame.origin.x = leftMarginImage;
} else {
CGFloat slideRight = (maxWidth - labelFrame.size.width) / 2.0;
labelFrame.origin.x = leftMarginText + slideRight;
checkFrame.origin.x = leftMarginImage + slideRight;
}
label.frame = labelFrame;
check.frame = checkFrame;
}
}
The commplete demo project can be found here: https://github.com/GayleDDS/TestCenteredLabel.git

Related

NSLayoutManager/NSTextContainer Ignores Scale Factor

I am attempting to measure an NSAttributedString's height given a constant width using the following method:
-(CGFloat)calculateHeightForAttributedString:(NSAttributedString*)attributedNotes {
CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:NSRegularControlSize scrollerStyle:NSScrollerStyleLegacy];
CGFloat width = self.tableView.frame.size.width - self.cellNotesWidthConstraint - scrollerWidth;
// http://www.cocoabuilder.com/archive/cocoa/54083-height-of-string-with-fixed-width-and-given-font.html
NSTextView *tv = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width - 20, 1e7)];
tv.font = [NSFont userFontOfSize:32];
[tv.textStorage setAttributedString:attributedNotes];
[self setScaleFactor:[self convertSliderValueToFontScale:self.fontScaleSlider] forTextView:tv];
[tv.layoutManager glyphRangeForTextContainer:tv.textContainer];
[tv.layoutManager ensureLayoutForTextContainer:tv.textContainer];
return [tv.layoutManager usedRectForTextContainer:tv.textContainer].size.height + 10.0f; // add a little bit of a buffer
}
Basically, the width is the table view's size minus the scroller and a little bit of each cell that is used to display other information. This method works really well as long as the text scale (via convertSliderValueToFontScale:) is 1.0. The result from usedRectForTextContainer is incorrect if I change the scale factor, however -- as if the scale factor was not being accounted for.
The scale is set in setScaleFactor:forTextView: on the NSTextView as follows (scalar is the actual scale amount):
[textView scaleUnitSquareToSize:NSMakeSize(scaler, scaler)];
Any ideas on how to fix this?
Edit: I've got a sample project to try here: Github. Strangely enough, things work if the scale is < 0, and they randomly seem to work in the 4.XXX range on occasion...
The answer turns out to be simple: add the NSTextView as a subview of an NSClipView with the same frame as the NSTextView.
The final height function is as follows:
-(CGFloat)calculateHeightForAttributedString:(NSAttributedString*)attributedNotes {
CGFloat width = self.textView.frame.size.width;
// http://www.cocoabuilder.com/archive/cocoa/54083-height-of-string-with-fixed-width-and-given-font.html
NSTextView *tv = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width, 1e7)];
tv.horizontallyResizable = NO;
tv.font = [NSFont userFontOfSize:32];
tv.alignment = NSTextAlignmentLeft;
[tv.textStorage setAttributedString:attributedNotes];
[self setScaleFactor:self.slider.floatValue forTextView:tv];
// In order for usedRectForTextContainer: to be accurate with a scale, you MUST
// set the NSTextView as a subview of an NSClipView!
NSClipView *clipView = [[NSClipView alloc] initWithFrame:NSMakeRect(0, 0, width, 1e7)];
[clipView addSubview:tv];
[tv.layoutManager glyphRangeForTextContainer:tv.textContainer];
[tv.layoutManager ensureLayoutForTextContainer:tv.textContainer];
return [tv.layoutManager usedRectForTextContainer:tv.textContainer].size.height;
}

How to subclass NSTextAttachment?

Here is my problem:
I use Core Data to store rich text input from iOS and/or OS X apps and would like images pasted into the NSTextView or UITextView to:
a) retain their original resolution, and
b) on display to be scaled to fit the textView correctly, which means scaling based on the size of the view on the device.
Currently I am using - (void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta to look for attachments and to then generate an image with a scale factor and assigning it to the textAttachment.image attribute.
This kind of works because I just change the scale factor and the original image gets retained but I believe a more elegant solution would be to use a NSTextAttachmentContainer subclass and to return from this an appropriately sized CGREct with
- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex
So my question is how do I create and insert such a subclass ?
Do I use the textStorage:didProcessEditing to iterate over each attachment and replace its NSTextAttachmentContainer with a class of my own, or can I simply create a Category and then somehow use this category to change the default behaviour. The latter seems much less intrusive but how do I get my textViews to automatically use this Category?
Oops: Just noticed NSTextAttachmentContainer is a protocol so I assume then creating a Category on NSTextAttachment and overriding the method above is an option.
Mmm: can't use Category to override an existing class method so I guess subclassing is the only option in which case how do I get the UITextView to use my attachment subclass, or do I have to iterate over the attributedString to replace all NSTextAttachments with instances of MYTextAttachment. And what will be the impact of unarchiving this string on OS X into say the default OS X NSTextAttachment (which is different from the iOS class) ?
Based on this excellent article, if you want to make use of
- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex
to scale an image text attachment, you have to create your own subclass of NSTextAttachment
#interface MYTextAttachment : NSTextAttachment
#end
with the scale operation in the implementation:
#implementation MYTextAttachment
- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex {
CGFloat width = lineFrag.size.width;
// Scale how you want
float scalingFactor = 1.0;
CGSize imageSize = [self.image size];
if (width < imageSize.width)
scalingFactor = width / imageSize.width;
CGRect rect = CGRectMake(0, 0, imageSize.width * scalingFactor, imageSize.height * scalingFactor);
return rect;
}
#end
based on
lineFrag.size.width
which give you (or what I have understood as) the width taken by the textView on which you have (will) set the attributed text "embedding" your custom text attachment.
Once the subclass of NSTextAttachment created, all you have to do is make use of it. Create an instance of it, set an image, then create a new attributed string with it and append it to a NSMutableAttributedText per example:
MYTextAttachment* _textAttachment = [MYTextAttachment new];
_textAttachment.image = [UIImage ... ];
[_myMutableAttributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:_immediateTextAttachment]];
For info it seems that
- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex
is called whenever the textview is asked to be relayout-ed.
Hope it helps, even though it doesn't answer every aspect of your problem.
Swift 3 (based on #Bluezen's answer):
class MyTextAttachment : NSTextAttachment {
override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
guard let image = self.image else {
return CGRect.zero
}
let height = lineFrag.size.height
// Scale how you want
var scalingFactor = CGFloat(0.8)
let imageSize = image.size
if height < imageSize.height {
scalingFactor *= height / imageSize.height
}
let rect = CGRect(x: 0, y: 0, width: imageSize.width * scalingFactor, height: imageSize.height * scalingFactor)
return rect
}
}
Note that I am scaling based on height, as I was getting a lineFrag width value of 10000000 in my particular use case. Also note that I replaced scalingFactor = ... with scalingFactor *= ... so that I could use an additional, non-unity scaling factor (0.8 in this case).

NSSplitView: Controlling divider position during window resize

I have an NSSplitView that's having two panes - a sidebar table view on the left and a web view on the right one. I also have a delegate set that's handling constraints for the sidebar like this:
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)dividerIndex {
return 500.0f;
}
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
return 175.0f;
}
- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview {
return NO;
}
It means that the sidebar can only be resized between 175 and 500 pixels and this works fine when using the divider handle. But when resizing the whole window the divider gets repositioned out of these constraints.
Does anybody know how to control this?
Additionally: If I want to store the user's choice of sidebar width, is it a good thought to read it out, save it to a preferences file and restore it later, or is there a more straight-forward way to do this? I noticed that the window's state gets saved in some cases - is this generally happening or do I have to control it?
Thanks in advance
Arne
I initially implemented the NSSplitView delegate functions and ended up with a lot of code to try to do something so simple as limit the minimum size for each of the split view sides.
I then changed my approach and found a clean and extremely simply solution. I simply set a auto layout constant for a width (>= to my desired minimum size) on the NSView for one side of the NSSplitView. I did the same on my other side. With these two simple constraints the NSSplitView worked perfectly without the need for delegate calls.
What you are looking for is:
- (void)splitView:(NSSplitView*)sender resizeSubviewsWithOldSize:(NSSize)oldSize
[sender frame] will be the new size of your NSSplitView after the resize. Then just readjust your subviews accordingly.
The problem is that when the NSSplitView itself is resized, -adjustSubviews gets called to do the work, but it plain ignores the min/max constraints from the delegate!
However -setPosition:ofDividerAtIndex: does take the constraints into account.
All you need to do is combine both - this example assumes an NSSplitView with only 2 views:
- (CGFloat)splitView:(NSSplitView*)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
return 300;
}
- (CGFloat)splitView:(NSSplitView*)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
return (splitView.vertical ? splitView.bounds.size.width : splitView.bounds.size.height) - 500;
}
- (void)splitView:(NSSplitView*)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
[splitView adjustSubviews]; // Use default resizing behavior from NSSplitView
NSView* view = splitView.subviews.firstObject;
[splitView setPosition:(splitView.vertical ? view.frame.size.width : view.frame.size.height) ofDividerAtIndex:0]; // Force-apply constraints afterwards
}
This appears to work fine on OS X 10.8, 10.9 and 10.10, and is much cleaner than the other approaches as it's minimal code and the constraints are not duplicated.
An alternative way to solve this is using splitView:shouldAdjustSizeOfSubview:
I've found this much simpler for my purposes.
For example, if you want to prevent the sidebarTableView from ever being smaller than your 175 minimum width then you can do something like this (assuming you made sidebarTableView an outlet on your view controller/delegate);
- (BOOL)splitView:(NSSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)subview
{
if ((subview==self.sidebarTableView) && subview.bounds.size.width<=175) {
return NO;
}
return YES;
}
Here's my implementation of -splitView:resizeSubviewsWithOldSize::
-(void)splitView:(NSSplitView *)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
if (![splitView isSubviewCollapsed:self.rightView] &&
self.rightView.frame.size.width < 275.0f + DBL_EPSILON) {
NSSize splitViewFrameSize = splitView.frame.size;
CGFloat leftViewWidth = splitViewFrameSize.width - 275.0f - splitView.dividerThickness;
self.leftView.frameSize = NSMakeSize(leftViewWidth,
splitViewFrameSize.height);
self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
0.0f,
275.0,
splitViewFrameSize.height);
} else
[splitView adjustSubviews];
}
In my case, rightView is the second of two subviews, which is collapsible with a minimum width of 275.0. leftView has no minimum or maximum and is not collapsible.
I used
- (void)splitView:(NSSplitView*)sender resizeSubviewsWithOldSize:(NSSize)oldSize
but instead of changing the subview frame, I used
[sender setPosition:360 ofDividerAtIndex:0]; //or whatever your index and position should be
Changing the frame didn't give me consistent results. Setting the position of the divider did.
Maybe too late for the party, however this is my implementation of resizeSubviewWithOldSize:. In my project I need a vertical resizable NSplitView with leftView width between 100.0 and 300.0; no 'Collapsing' taken in account. You should take care of all possible dimensions for the subviews.
-(void)splitView:(NSSplitView *)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
if (self.leftView.frame.size.width >= kMaxLeftWidth) {
NSSize splitViewFrameSize = splitView.frame.size;
CGFloat leftViewWidth = kMaxLeftWidth;
CGFloat rightViewWidth = splitViewFrameSize.width - leftViewWidth - splitView.dividerThickness;
self.leftView.frameSize = NSMakeSize(leftViewWidth,
splitViewFrameSize.height);
self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
0.0f,
rightViewWidth,
splitViewFrameSize.height);
} else if (self.leftView.frame.size.width <= kMinLeftWidth) {
NSSize splitViewFrameSize = splitView.frame.size;
CGFloat leftViewWidth = kMinLeftWidth;
CGFloat rightViewWidth = splitViewFrameSize.width - leftViewWidth - splitView.dividerThickness;
self.leftView.frameSize = NSMakeSize(leftViewWidth,
splitViewFrameSize.height);
self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
0.0f,
rightViewWidth,
splitViewFrameSize.height);
} else {
NSSize splitViewFrameSize = splitView.frame.size;
CGFloat leftViewWidth = self.leftView.frame.size.width;
CGFloat rightViewWidth = splitViewFrameSize.width - leftViewWidth - splitView.dividerThickness;
self.leftView.frameSize = NSMakeSize(leftViewWidth,
splitViewFrameSize.height);
self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
0.0f,
rightViewWidth,
splitViewFrameSize.height);
}
}
I've achieved this behavior by setting the holdingPriority on NSSplitViewItem to Required(1000) for the fixed side in Interface Builder. You can then control the width for the fixed side by setting a constraint on the underlying NSView.
I just needed to do this, and came up with this method which is a bit simpler than previous examples. This code assumes there are IBOutlets for the left and right NSScrollViews of the NSSplitView container, as well as CGFloat constants for the minimum size of the left and right views.
#pragma mark - NSSplitView sizing override
//------------------------------------------------------------------------------
// This is implemented to ensure that when the window is resized that our main table
// remains at it's smallest size or larger (the default proportional sizing done by
// -adjustSubviews would size it smaller w/o -constrainMinCoordiante being called).
- (void) splitView: (NSSplitView *)inSplitView resizeSubviewsWithOldSize: (NSSize)oldSize
{
// First, let the default proportional adjustments take place
[inSplitView adjustSubviews];
// Then ensure that our views are at least their min size
// *** WARNING: this does not handle allowing the window to be made smaller than the total of the two views!
// Gather current sizes
NSSize leftViewSize = self.leftSideView.frame.size;
NSSize rightViewSize = self.rightSideView.frame.size;
NSSize splitViewSize = inSplitView.frame.size;
CGFloat dividerWidth = inSplitView.dividerThickness;
// Assume we don't have to resize anything
CGFloat newLeftWidth = 0.0f;
// Always adjust the left view first if we need to change either view's size
if( leftViewSize.width < kLeftSplitViewMinSize )
{
newLeftWidth = kLeftSplitViewMinSize;
}
else if( rightViewSize.width < kRightSplitViewMinSize )
{
newLeftWidth = splitViewSize.width - (kRightSplitViewMinSize + dividerWidth);
}
// Do we need to adjust the size?
if( newLeftWidth > 0.0f )
{
// Yes, do so by setting the left view and setting the right view to the space left over
leftViewSize.width = newLeftWidth;
rightViewSize.width = splitViewSize.width - (newLeftWidth + dividerWidth);
// We also need to set the origin of the right view correctly
NSPoint origin = self.rightSideView.frame.origin;
origin.x = splitViewSize.width - rightViewSize.width;
[self.rightSideView setFrameOrigin: origin];
// Set the the ajusted view sizes
leftViewSize.height = rightViewSize.height = splitViewSize.height;
[self.leftSideView setFrameSize: leftViewSize];
[self.rightSideView setFrameSize: rightViewSize];
}
}

Is there a "right" way to have NSTextFieldCell draw vertically centered text?

I have an NSTableView with several text columns. By default, the dataCell for these columns is an instance of Apple's NSTextFieldCell class, which does all kinds of wonderful things, but it draws text aligned with the top of the cell, and I want the text to be vertically centered in the cell.
There is an internal flag in NSTextFieldCell that can be used to vertically center the text, and it works beautifully. However, since it is an internal flag, its use is not sanctioned by Apple and it could simply disappear without warning in a future release. I am currently using this internal flag because it is simple and effective. Apple has obviously spent some time implementing the feature, so I dislike the idea of re-implementing it.
So; my question is this: What is the right way to implement something that behaves exactly like Apple's NStextFieldCell, but draws vertically centered text instead of top-aligned?
For the record, here is my current "solution":
#interface NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical;
#end
#implementation NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical
{
#try { _cFlags.vCentered = centerVertical ? 1 : 0; }
#catch(...) { NSLog(#"*** unable to set vertical centering"); }
}
#end
Used as follows:
[[myTableColumn dataCell] setVerticalCentering:YES];
The other answers didn't work for multiple lines. Therefore I initially continued using the undocumented cFlags.vCentered property, but that caused my app to be rejected from the app store. I ended up using a modified version of Matt Bell's solution that works for multiple lines, word wrapping, and a truncated last line:
-(void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSAttributedString *attrString = self.attributedStringValue;
/* if your values can be attributed strings, make them white when selected */
if (self.isHighlighted && self.backgroundStyle==NSBackgroundStyleDark) {
NSMutableAttributedString *whiteString = attrString.mutableCopy;
[whiteString addAttribute: NSForegroundColorAttributeName
value: [NSColor whiteColor]
range: NSMakeRange(0, whiteString.length) ];
attrString = whiteString;
}
[attrString drawWithRect: [self titleRectForBounds:cellFrame]
options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin];
}
- (NSRect)titleRectForBounds:(NSRect)theRect {
/* get the standard text content rectangle */
NSRect titleFrame = [super titleRectForBounds:theRect];
/* find out how big the rendered text will be */
NSAttributedString *attrString = self.attributedStringValue;
NSRect textRect = [attrString boundingRectWithSize: titleFrame.size
options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin ];
/* If the height of the rendered text is less then the available height,
* we modify the titleRect to center the text vertically */
if (textRect.size.height < titleFrame.size.height) {
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - textRect.size.height) / 2.0;
titleFrame.size.height = textRect.size.height;
}
return titleFrame;
}
(This code assumes ARC; add an autorelease after attrString.mutableCopy if you use manual memory management)
Overriding NSCell's -titleRectForBounds: should do it -- that's the method responsible for telling the cell where to draw its text:
- (NSRect)titleRectForBounds:(NSRect)theRect {
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
return titleFrame;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSRect titleRect = [self titleRectForBounds:cellFrame];
[[self attributedStringValue] drawInRect:titleRect];
}
For anyone attempting this using Matt Ball's drawInteriorWithFrame:inView: method, this will no longer draw a background if you have set your cell to draw one. To solve this add something along the lines of
[[NSColor lightGrayColor] set];
NSRectFill(cellFrame);
to the beginning of your drawInteriorWithFrame:inView: method.
FYI, this works well, although I haven't managed to get it to stay centered when you edit the cell... I sometimes have cells with large amounts of text and this code can result in them being misaligned if the text height is greater then the cell it's trying to vertically center it in. Here's my modified method:
- (NSRect)titleRectForBounds:(NSRect)theRect
{
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
// test to see if the text height is bigger then the cell, if it is,
// don't try to center it or it will be pushed up out of the cell!
if ( titleSize.height < theRect.size.height ) {
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
}
return titleFrame;
}
No. The right way is to put the Field in another view and use auto layout or that parent view's layout to position it.
Though this is pretty old question...
I believe default style of NSTableView implementation is intended strictly for single line text display with all same size & font.
In that case, I recommend,
Set font.
Adjust rowHeight.
Maybe you will get quietly dense rows. And then, give them padding by setting intercellSpacing.
For example,
core_table_view.rowHeight = [NSFont systemFontSizeForControlSize:(NSSmallControlSize)] + 4;
core_table_view.intercellSpacing = CGSizeMake(10, 80);
Here what you'll get with two property adjustment.
This won't work for multi-line text, but very good enough for quick vertical center if you don't need multi-line support.
I had the same problem and here is the solution I did :
1) In Interface Builder, select your NSTableCellView. Make sure it as big as the row height in the Size Inspector. For example, if your row height is 32, make your Cell height 32
2) Make sure your cell is well placed in your row (I mean visible)
3) Select your TextField inside your Cell and go to your size inspector
4) You should see "Arrange" item and select "Center Vertically in Container"
--> The TextField will center itself in the cell

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