Cocoa Sample Code: SimplePicture
In the ongoing quest to remember that not everyone is a Cocoa expert, I put together a small image viewing app which shows you a number of basic Cocoa techiques. Everyone, meet SimplePicture. SimplePicture — everyone.Download SimplePicture Project for Xcode 2.4
SimplePicture is a small document-based Cocoa app which loads all of the images from a given folder and displays them as a list of thumbnails and file names in a table view. Clicking on an image will display it in the main image view.
Since this is a document-based app, you can open multiple viewer windows at a time, each displaying a different list of images.
What This Sample Covers
This sample code focuses on the following classes:
- NSDocument
- NSDocumentController
- NSImage
- NSImageView
- NSFileManager
The project involves these concepts:
- Cocoa Bindings
- Basic Categories
- Very basic threading
- Using images in a table view
- Basic memory management
- Subclassing built-in classes
- Using NSApplication delegate messages
- Drawing into an offscreen NSImage buffer
- Providing a custom NSDocumentController subclass
- Allowing the user to select a folder in NSOpenPanel
How it Works
Once the user chooses a folder to load images from using File → Open, SimplePicture uses NSFileManager to create an instance of NSDirectoryEnumerator. The enumerator allows us to recursively loop through all file names and subfolders in the folder.
We don't want to try to open each an every file blindly, so we need to figure out which files are likely to be images. To do this, we use a function from ImageIO called CGImageSourceCopyTypeIdentifiers(). This function returns an array of UTI identifiers for images formats ImageIO can handle.
For each file in the folder, we convert the file extension to a UTI indentifier using UTTypeCreatePreferredIdentifierForTag(). We compare that UTI identifier to the array of image types, and see if there's a match. If there is, create an NSImage from that file.
All of this is done in a separate thread so the UI stays reasonably responsive during load time. The spinner animates in the foreground while all the processing happens in the background.
Things That Make This Sample Code
The progress indicator is an indeterminate spinner, so the user has no idea how long the loading process will take. For folders with a lot of files, this could take a very long time.
SimplePicture does not put any limits on the size or number of images loaded, which could easily result in the application quitting unexepectedly if it runs out of resources. It could also get very slow as it runs out of real memory and starts swapping.
There's no concept of lazy loading, also known as "faulting." Ideally, if there are 30,000 images in the selected folder, we should load only certain portions of that list into memory at a time. Not only does this mean the app is more responsive, but it means we won't run out of memory.
Scalability is not a simple thing to address, which is why something like Core Data is so incredibly value to a Mac programmer. It's also a much more advanced topic than we're looking to tackle here.
That said, there are some simpler improvements we could make which would improve the experience and functionality. If you guys and girls find this interesting, maybe we can iterate on this a bit.
Cocoa Sample Code: SimplePicture
Posted Sep 29, 2007 — 24 comments below
Posted Sep 29, 2007 — 24 comments below
Qwerty Denzel — Sep 30, 07 4665
I once made a small image viewer that uses OpenGL to display the images (Quartz's ImageIO for loading), which had simple tools for grab-scrolling and zooming.
I wasn't quite happy with it for a couple of reasons. One was that it didn't anti-alias properly (which could be solved by scaling with Quartz 2D or vImage, but this negates the purpose of using OpenGL). It also consumed a lot of memory for large images, so I wanted to find a process of loading the bitmap tiles (the image already needs to be chopped-up to fit into texture-sized pieces) so that they were loaded dynamically, instead of all at once.
I'll post an example here if you like, I'll just need to finish some work off first though. :)
Oh, and those thumbnails look a bit rough. If only we had some way of smoothing them�
Nikolas Schrader — Sep 30, 07 4666
Blain — Sep 30, 07 4667
Scott Stevenson — Sep 30, 07 4668
How about using Quartz Composer?
Oh, and those thumbnails look a bit rough. If only we had some way of smoothing them�
I tried doing that, but it was super slow with a lot of images. That was early in the process, though -- possibly before I added the custom thumbnail generation. Could be something to fix in the next version.
Qwerty Denzel — Sep 30, 07 4669
I admit I haven't explored it much, but that's mainly because for this idea I was trying to target older hardware. I think the antialias method that QC uses is the same as that of OpenGL 2.0 (since that is what it renders with), which again is only compatible with newer hardware. I think the maximum bitmap size in Quartz Composer is constrained to that of an OpenGL texture - 1024 to several thousand pixels square, depending on the hardware.
I look forward to the new version; and on that note, new tutorials, especially with Leopard coming up hopefully soon. I wouldn't mind contributing in some way (but I am not a LeopardKit developer).
Do you have any plans for new tutorials at Cocoa Dev Central?
Cocoa Novice — Sep 30, 07 4670
As you spoke of improving experience and iterating, the first thing which would sort of annoy me if that'd be a real application:
Scroll down the NSTableView and click one of the downmost pictures, now resize the split view so the NSTableView on the left gets completely hidden. Next resize the split view again, so the NSTableView is visible again - and you're at the top of the NSTableView again, instead of the position before resizing/hiding.
I am interested in how to fix it and I think it might be interesting for other Cocoa beginners, too.
Qwerty Denzel — Sep 30, 07 4671
You could try implementing the NSSplitView delegate method -splitViewDidResizeSubviews: to test whether the first subview (NSTableView and its scroll-view in this case) is collapsed (using -isSubviewCollapsed:), and if so then change an instance variable to store the scroll position ([[[tableView enclosingScrollView] verticalScroller] floatValue]), which you probably would have had to have gotten already in -splitViewWillResizeSubviews.
Whew. I hope that helps (and that it's the best method).
Rosyna — Sep 30, 07 4673
Scott Stevenson — Sep 30, 07 4674
You could probably do this in the -splitViewDidResizeSubviews: method that Qwerty Denzel mentions.
Scott Stevenson — Sep 30, 07 4675
If it was anyone else, I'd think you were serious. For the benefit of everyone else, though, the function is part of the ImageIO framework, which is not deprecated in any way.
In any case, this is sample code for Tiger. :)
Charles — Oct 02, 07 4699
Charles — Oct 02, 07 4700
Coriander — Oct 02, 07 4701
The name of the app is Coriander Collage. (As a warning, the website contains adult content.)
Blain — Oct 02, 07 4703
Why oh why can't you just replace the images with fuzzy kittens, say it's a hands-free media viewer, and act innocent, like what Safari does with Private Browsing?
(I kid because I care.)
Scott Stevenson — Oct 03, 07 4704
Coriander — Oct 03, 07 4709
if ([self inLiveResize]) { [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationLow]; } else { [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; }
----------
Why oh why can't you just replace the images with fuzzy kittens
How about a skateboarding dog?
http://www.coriandersoftware.com/screenshots
http://www.coriandersoftware.com/video_clip
(These were the original screenshots and video.)
Blain — Oct 03, 07 4711
[self performSelectorOnMainThread: @selector(setImageList:) withObject: imageList waitUntilDone: NO]; [imageList release]; [pool release];
Since we're not waiting until done, do we run into any race conditions of imageList, and any images in the pool, being fully released before setImageList: is run?
Is there any advantage to having the thread end ASAP? In other words, if waitUntilDone is YES, does having an inactive thread around longer hurt things? I'm looking mostly at pool release, which apparently is fast. I know this is a mostly trivial question in this case, but I'm thinking about responsiveness/speed tradeoffs in my own multithreaded app.
@Coriander: Good point about inLiveResize.
Offtopic: Interesting about the original screenshots. I'd ask about how the shift in advertising's affected things, but this isn't the place.
Scott Stevenson — Oct 04, 07 4712
You know, I actually messed around with that but couldn't get the results I was expecting. I didn't spend a lot of time on it, though.
(These were the original screenshots and video.)
This is actually pretty interesting. A sort of mixed photo/video lightbox. It has possibilities beyond what you suggested. The video does a good job of illustrating this.
For the record, I didn't have any problem at all with you discussing your app or or its implementation (which is relevant), I was just trying to avoid a side discussion which would have been vastly off topic.
@Blain: Is there any advantage to having the thread end ASAP?
None I can think of. Good point.
Coriander — Oct 04, 07 4713
The effect can be greater by using NSImageInterpolationNone instead of NSImageInterpolationLow.
Also, remember to refresh the view at the end of live resizing to bring it back to high image quality:
- (void)viewDidEndLiveResize { [super viewDidEndLiveResize]; [self setNeedsDisplay:YES]; }
Brian — Oct 24, 07 4823
thanks!
Scott Stevenson — Oct 24, 07 4824
The binding which is used on the NSImageView subclass is "Value Path" and it's bound to "imagePath" of the currently selection, which is an SPImage object.
This means the view is not bound to an image per se, but rather to the filesystem path to the image. Given that path, NSImageView (and the custom subclass) knows open to open the file and display the image.
StuFF mc — Nov 11, 07 5054
But... What would it take you to "update/refresh" this post for Leopard ? I mean, sparing me some "pains" of Tiger ;) I'll ofcourse develop a Leopard Only app, what else ?! ;)
Cheers !
Eneko Alonso — Dec 08, 07 5184
toby — Apr 11, 08 5713
That skateboarding dog is awesome. I've not seen one of those before.