I have a view with several hundred individual UILabels on it. It works, but it is clearly a bog on the system. The app is sluggish until another method releases this view with all the labels.
However, I know that it is necessary often to put this amount of text on a screen, so I'm wondering how is a better way to do it. The labels are each placed in a specific location based on other parameters, so it cannot just be one big textview.
I am guessing the better way to do this is to "draw" the text on the screen using an Open GL Font or something like that. Any other suggestions or advice?
You can also check Additions to NSString: NSString UIKit Additions Sample usage:
- (void)drawRect:(CGRect)rect
{
NSString *temperature = [NSString stringWithFormat:#"%#\n%d / %d", weatherItem.place, high, low];
[[UIColor blackColor] set];
[temperature drawInRect:CGRectMake(15.0, 5.0, 50.0, 40.0) withFont:[UIFont systemFontOfSize:11.0]];
}
Use Core Graphics to draw the text onto the CGContext in your drawRect method. See the Drawing Text section of the CGContext docs
Related
I'm making a tool that will pull data from a .csv and create a grid of images with captions [like "This"] in Cocoa, then export that to a PDF. I do not need to actually display the view, just save a file. As a complete beginner to drawing programmatically, I have some questions about the process:
What class should I use? I'm assuming NSView, but like I said I've never done this before so I'm not sure.
Do I need to specify the pixel coordinates for every single object, or can I make each object relative to another in some way?
How do I create separate pages for the view?
Keep in mind that I read the Apple guides, and while it had some helpful tidbits, overall it was unusually hard for me to comprehend. If someone could explain in layman's terms what I need to know it would be very appreciated! Thank you in advance.
Have a look at NSCollectionView
Overview
NSCollectionView class displays an array of content as a grid of
views. The views are specified using the NSCollectionViewItem class
which makes loadings nibs containing the view easy, and supports
bindings
There are lots of tutorials.
Including:
Cocoa Programming L42 - NSCollectionView
And
Apples own quick guide to Collection Views
And maybe also look at NSDocuments
Overview
The NSDocument abstract class defines the interface for OS X
documents. A document is an object that can internally represent data
displayed in a window and that can read data from and write data to a
file or file package. Documents create and manage one or more window
controllers and are in turn managed by a document controller.
Documents respond to first-responder action messages to save, revert,
and print their data.
Conceptually, a document is a container for a body of information
identified by a name under which it is stored in a disk file. In this
sense, however, the document is not the same as the file but is an
object in memory that owns and manages the document data. In the
context of AppKit, a document is an instance of a custom NSDocument
subclass that knows how to represent internally, in one or more
formats, persistent data that is displayed in windows.
A document can read that data from a file and write it to a file. It
is also the first-responder target for many menu commands related to
documents, such as Save, Revert, and Print. A document manages its
window’s edited status and is set up to perform undo and redo
operations. When a window is closing, the document is asked before the
window delegate to approve the closing.
NSDocument is one of the triad of AppKit classes that establish an
architectural basis for document-based apps (the others being
NSDocumentController and NSWindowController).
Figured it out a few days ago, thought I'd come back to answer for anyone else with the same question.
What class should I use? I'm assuming NSView, but like I said I've never done this before so I'm not sure.
NSView is in fact the class I used to draw each page.
Do I need to specify the pixel coordinates for every single object, or can I make each object relative to another in some way?
I did end up specifying the pixel coordinates for each image on the grid (plus its caption), but it was easy to calculate where they should be placed once I learned the size of a 8.50 x 11 inch page in points. The next challenge was drawing them in a for loop rather than having to explicitly declare each possible NSRect. Here's my code in drawRect:
// Declared elsewhere: constants for horizontal/vertical spacing,
// the width/height for an image, and a value for what row the image
// should be drawn on
for (int i = 0; i < [_people count]; i++) {
float horizontalPoint = 0.0; // What column should the image be in?
if (i % 2 != 0) { // Is i odd? (i.e. Should the image be in the right column?)
horizontalPoint += (imageWidth + horizontalSpace); // Push it to the right
}
NSRect imageRect = NSMakeRect(horizontalSpace + horizontalPoint, verticalSpace + verticalPoint,
imageWidth, imageHeight);
// Draw the image with imageRect
if (i % 2 != 0) { // Is i odd? (i.e. Is the current row drawn?)
verticalPoint = (imageRect.origin.y + imageRect.size.height); // Push the row down
}
}
I do realize that I could've coded that more efficiently (e.g. making a BOOL for i % 2 != 0), but I was rushing the whole project because my friend who needed it was on a deadline.
How do I create separate pages for the view?
With some googling, I came up with this SO answer. However, this wasn't going to work unless I had one big view with all the pages concatenated together. I came up with a way to do just that:
// Get an array of arrays containing 1-6 JANPerson objects each using data from a parsed in .csv
NSArray *paginatedPeople = [JANGridView paginatedPeople:people];
int pages = [JANGridView numberOfPages:people];
// Create a custom JANFlippedView (just an NSView subclass overriding isFlipped to YES)
// This will hold all of our pages, so the height should be the # of pages * the size of one page
JANFlippedView *view = [[JANFlippedView alloc] initWithFrame:NSMakeRect(0, 0, 612, 792 * pages)];
for (int i = 0; i < [paginatedPeople count]; i++) { // Iterate through each page
// Create a custom JANGridView with an array of people to draw on a grid
JANGridView *gridView = [[JANGridView alloc] initWithFrame:NSMakeRect(0, 0, 612, 792) people:paginatedPeople[i]];
// Push the view's frame down by 792 points for each page drawn already
// and add it to the main view
gridView.frame = NSMakeRect(0, 792 * i, gridView.frame.size.width, gridView.frame.size.height);
[view addSubview:gridView];
}
I apologize if this is hard to understand for anybody; I'm better at talking through my process than writing! I welcome anyone to ask for help if there's something unclear, or edit if they can make it better.
NsView; so tis a mac app?
CGPointMake Returns a point with the specified coordinates. i.e. placing an image in a specific spot on the screen using matrices i.e.
layer.position = CGPointMake ([self view].bounds.size.width /2, [self view].bounds.size.height /3 );
(this example is oriented around core animation (moving objects on screen so please don't take it too literally) hence the layer attribute)
Also this line
layer.bounds= CGRectMake (100,100,1000,1000);
specifies a rectangles boundaries (rectangles can be filled with images and custom data using a bridge i believe; like this):
UIImage *image2 = [[UIImage alloc]initWithContentsOfFile:[[NSBundle mainBundle]pathForResource:#"flogo#2x"ofType:#"png"]];
layer.contents = (__bridge id)image2.CGImage;
Also i believe the cgdrawrect class when combined with matrices i.e. (x,x,x,x) can draw custom rectangles as in your image.
But hopefully you catch my drift with drawing and substituting images . The Core graphics framework will probably be used here. ( my whole answer used core animation as a reference)
HEADS-UP: I write way too much stuff. For the folks who don't want to read a bunch of whining, feel free to skip to the TL;DR at the bottom of the page. Thanks!
For the last few weeks, I've been subclassing all of the standard Aqua controls to make them look good on dark backgrounds, panels, and windows. I'm currently pretty far in, and for the most part, things are lookin' good. However, I keep running into the same problems over and over again. They're almost always math-related because I was big screwup in school. (I was that kid who would dismissively ask, "When will I ever use a stem-and-leaf plot in the real world? Ha!" In other words, I bought my TI-86 for Block Dude and Drug Wars. Anyway, I digress …
So the problem that keeps cropping up is my ineptitude when it comes to working with multiple coordinate systems. Specifically, working with Auto Layout in flipped coordinate systems. By default, NSView objects use Cartesian coordinates. However, most — if not all — of the NSControl classes flip their coordinates for drawing. Knowing that, this is what I've been doing:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
// -isHUDPanel is just a method I added to NSWindow via category.
if ( controlView.window.isHUDPanel ) {
[self drawCustomWithFrame:cellFrame inView:controlView];
} else {
[super drawWithFrame:cellFrame inView:controlView];
}
}
If the control's window has the NSHUDWindowMask bit set, the -drawWithFrame:inView: method kicks it over to my custom method:
- (void)drawCustomWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSRect drawingRect = [controlView alignmentRectForFrame:cellFrame];
// ...
// Where the magic happens
// ...
}
Auto Layout still baffles me, so I'm not even sure I'm using -alignmentRectForFrame: in the proper context, but it makes sense to me to use it to align stuff when drawing, so that's what I've been doing. Anyway, the problem I've been seeing sometimes when comparing my control side-by-side to a native control is that mine is 1–2 points "off" on one axis or another. Sometimes they align perfectly. Sometimes one axis is great and the other is whacky. It's a crapshoot. Then there are other times when the controls' frames will align just right, but the baselines of the controls' text will be a few points off. It's really maddening.
I finally decided to try some stuff, and it seems as though my alignment troubles may be stemming from the way NSEdgeInsets work. For example, let's say my control's bounds look like this:
NSRect: {{0, 0}, {150, 22}}; // (aka "cellFrame" in the drawing methods)
In my -drawWithFrame:inView: implementation, I first get the control's alignment rectangle by calling [controlView alignmentRectForFrame:cellFrame], which returns, let's say …
NSRect: {{2, 3}, {146, 19}}
At first glance, it seems as though the edges on the x-axis are inset by 2.0, and the y origin is 3.0. With that quick math out of the way, I now know everything I need to get started building NSBezierPath objects and going to work. But wait … Is that y origin in the control's coordinate system (flipped) or the default coordinate system (not flipped)? Surely it's in the control's, right? Well, I decided to test this out by manually setting the alignmentRect via the control's -alignmentRectInsets like so:
NSEdgeInsets insets = self.controlView.alignmentRectInsets;
NSRect bounds = self.controlView.bounds;
NSRect alignmentRect = bounds;
alignmentRect.origin.x += insets.left;
alignmentRect.origin.y += (self.controlView.isFlipped) ? insets.top : insets.bottom;
alignmentRect.size.width -= insets.left + insets.right;
alignmentRect.size.height -= insets.top + insets.bottom;
When manually creating the alignmentRect myself, my controls lined up with the native ones! I just don't get it though. Why aren't the controls' -alignmentRectForFrame: methods returning their alignmentRects whilst respecting flippedness of the view? I'm not even sure if they all do or not, but from the two controls I've tested this little trick on, the alignment has been on the money.
I've searched all over the web for some insight on this, but nobody seems to have the weird geometry problems I have. I always make these questions way too long, too, and I apologize. I just can't explain my problem when I don't exactly know my problem. :-(
TL;DR: How do I get the proper alignment rectangle for drawing in an NSControl subclass? Is there even a "standard" way? And finally, how does -cellSize, -cellSizeForBounds, -baselineOffsetFromBottom, and -intrinsicContentSize all fit together in the grand scheme of things? And final question: Auto Layout … WTF?
Here's what my app does.
It requests about 100 images from the server when it starts up. It stores
those images in model objects, we'll call them modelObject.storedImage
The modelObject also has a viewObject, which is a UIImageView subclass
The app has a 2D scrollview that sets all 100 viewObjects as subviews, with up to 16 of those shown in the scrollview content area at any give time
When you scroll to a given area, it finds the 16 images from the appropriate
modelObject.storedImage and does
modelObject.viewObject.image = modelObjects.storedImage;
When a viewObject scrolls out of the content area, it is purged from the view
[modelObject.viewObject setImage:nil];
[modelObject.viewObject removeFromSuperview];
The problem is, despite the purging, memory keeps growing as you scroll to new areas of the scrollview content area. Memory from putting the former modelObject.viewObject.images on the scrollview is not released. So if you scroll to enough new areas, memory use gets out of control.
To be clear, when the app first loads the 100 images into the modelObject.storedImages, memory does not get out of control. Furthermore, if I comment this out
//modelObject.viewObject.image = modelObjects.storedImage;
then memory also stays fine (but of course the scrollviews subviews don't show any image).
I did see this question "Setting uiimage to nil doesn't release memory with ARC" but I believe it's different because in my app I can't afford to keep requesting images over and over from the server. They must sit at modelObject.storedImage once they've been requested, always ready to be displayed when their corresponding viewObject is scrolled to.
Surely there must be a way to tell ARC to release the memory for these nil images? Thanks in advance for your help!
If images are created by imageNamed:, then no way to manually to release memory, you can optimize it by imageWithContentsOfFile, thanks to you are using images for server, I think you used imageWithContentsOfFile. :)
You can try use #autoreleasepool {} at scrolling method to release memory in real time.
You can try release viewObject instead of removeFromSuperview, maybe what you should store is viewFrame rather than viewObject.
Hope offer you some help.
So I went around this problem and did this as a solution. I'd be curious to see if someone has a more robust idea. I made a copy of the image and set the copy on the scrollview. Now when I purge that copy, its memory is actually released (thank you!!), while the original image still sits comfortably in the model. Note, it did not work to simply copy the image's CGImage and make a copy off that, in which case memory did not release. I had to make a new copy using a UIGraphicsImageContext.
Code to set the image on Scrollview:
UIImage *imageToCopy = modelObject.imageStore;
CGSize size = imageToCopy.size;
UIGraphicsBeginImageContext(size);
[imageToCopy drawInRect:CGRectMake(0, 0, imageToCopy.size.width, imageToCopy.size.height)];
UIImage *copiedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[modelObject.viewObject setImage:copiedImage];
Code to purge when it's off the scrollview's content area:
[modelObject.viewObject setImage:nil];
[modelObject.viewObject removeFromSuperview];
The only problem was that drawing those image contexts used a lot of memory, so I added this modification to make the drawing smaller:
...
CGSize size = imageToCopy.size;
size.width *= .2;
size.height *= .2;
UIGraphicsBeginImageContext(size);
[imageToCopy drawInRect:CGRectMake(0, 0, imageToCopy.size.width*.2, imageToCopy.size.height*.2)];
...
I have two PNGs in a Mac project. Normal and #2x. Xcode combines these into a single TIFF with the #2x being at index 0 and the #1x at index 1.
What is the suggested approach to get the appropriate image as CGImageRef version (for use with Quartz) for the current display scale?
I can get the image manually via CGImageSource:
NSBundle *mainBundle = [NSBundle mainBundle];
NSURL *URL = [mainBundle URLForResource:#"Canvas-Bkgd-Tile" withExtension:#"tiff"];
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)(URL), NULL);
_patternImage = CGImageSourceCreateImageAtIndex(source, 1, NULL); // index 1 is #1x, index 0 is #2x
CFRelease(source);
I also found this to be working, but I am not certain that this will return the Retina version on a Retina display:
NSImage *patternImage = [NSImage imageNamed:#"Canvas-Bkgd-Tile.tiff"];
_patternImage = [patternImage CGImageForProposedRect:NULL context:nil hints:nil];
CGImageRetain(_patternImage); // retain image, because NSImage goes away
An acceptable answer to this question either provides a solution how to get the CGImage suitable from a combined multi-resolution TIFF, or explains why the second approach here is working. Or what changes are required.
I am opting to answer on "why the second approach here is working".
In one of the WWDC videos published since 2010, they said that :
+[NSImage imageNamed:] chooses the best image representation object available for the current display.
So chances are that you are calling this class method from within a locked focus context (e.g. within a drawRect: method or similar), or maybe you actually called lockFocus yourself. Anyway, the result is that you get the most suitable image. But only when calling +[NSImage imageNamed:].
EDIT: Found it here:
http://adcdownload.apple.com//wwdc_2012/wwdc_2012_session_pdfs/session_213__introduction_to_high_resolution_on_os_x.pdf
Search for the keyword "best" in the slides: "NSImage automatically chooses best representation […]".
So, your second version will return the Retina version on a Retina display, you can be certain of it, it is advertised in the documentation[*].
[*] This will only work if you provide valid artwork.
In my application I needed something like a particle system so I did the following:
While the application initializes I load a UIImage
laserImage = [UIImage imageNamed:#"laser.png"];
UIImage *laserImage is declared in the Interface of my Controller. Now every time I need a new particle this code makes one:
// add new Laserimage
UIImageView *newLaser = [[UIImageView alloc] initWithImage:laserImage];
[newLaser setTag:[model.lasers count]-9];
[newLaser setBounds:CGRectMake(0, 0, 17, 1)];
[newLaser setOpaque:YES];
[self.view addSubview:newLaser];
[newLaser release];
Please notice that the images are only 17px * 1px small and model.lasers is a internal array to do all the calculating seperated from graphical output. So in my main drawing loop I set all the UIImageView's positions to the calculated positions in my model.lasers array:
for (int i = 0; i < [model.lasers count]; i++) {
[[self.view viewWithTag:i+10] setCenter:[[model.lasers objectAtIndex:i] pos]];
}
I incremented the tags by 10 because the default is 0 and I don't want to move all the views with the default tag.
So the animation looks fine with about 10 - 20 images but really gets slow when working with about 60 images. So my question is: Is there any way to optimize this without starting over in OpenGl ES?
As jeff7 and FenderMostro said, you're using the high-level API (UIKit), and you'd have better performance using the lower APIs, either CoreAnimation or OpenGL. (cocos2d is built on top of OpenGL)
Your best option would be to use CALayers instead of UIImageViews, get a CGImageRef from your UIImage and set it as the contents for these layers.
Also, you might want to keep a pool of CALayers and reuse them by hiding/showing as necessary. 60 CALayers of 17*1 pixels is not much, I've been doing it with hundreds of them without needing extra optimization.
This way, the images will already be decompressed and available in video memory. When using UIKit, everything goes through the CPU, not to mention the creation of UIViews which are pretty heavy objects.
Seems like you're trying to code a game by using the UIKit API, which is not really very suitable for this kind of purpose. You are expending the device's resources whenever you allocate a UIView, which incurs slowdowns because object creation is costly. You might be able to obtain the performance you want by dropping to CoreAnimation though, which is really good at drawing hundreds of images in a limited time frame, although it would still be much better if you used OpenGL or an engine like Cocos2d.
The UIImageView is made to display single OR multiple images. So, instead of creating every time a UIImageView, you should consider creating a new image and add it to the UIImageView instead.
See here.
I'd recommend starting over using OpenGL ES, there is an excellent framework called cocos2d for iPhone that can make this type of programming very easy and fast. From a quick look at your code, you're lasers can be remodeled as CCSprite which is an easy way to move images around a scene among many other things.