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.

SimplePicture Screenshot


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.
Design Element
Cocoa Sample Code: SimplePicture
Posted Sep 29, 2007 — 24 comments below




 

Qwerty Denzel — Sep 30, 07 4665

This tutorial covers a great set of concepts for the beginner. Thanks Scott!

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

Great application - I just started reading books about Cocoa - itīs really great, and every time I see what great applicationīs are built with Cocoa, I want to built something like that myself...

Blain — Sep 30, 07 4667

Heh. A week or so ago, I was tempted to make an image editing program, something bare bones, akin to TextEdit, so that everyone and their brother could make their own image editor.

Scott Stevenson — Sep 30, 07 4668 Scotty the Leopard

@Qwerty Denzel: I wasn't quite happy with it for a couple of reasons.

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

How about using Quartz Composer?
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

Thanks very much for that interesting tutorial!

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

@Cocoa Novice

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

CGImageSourceCopyTypeIdentifiers()? Isn't Carbon deprecated in 10.5 or something?

Scott Stevenson — Sep 30, 07 4674 Scotty the Leopard

@Cocoa Novice, @Qwerty Denzel: NSTableView, thankfully, makes this pretty easy. There's a -scrollRowToVisible: method, so all you need to do is get the current selection index and scroll to it.

You could probably do this in the -splitViewDidResizeSubviews: method that Qwerty Denzel mentions.

Scott Stevenson — Sep 30, 07 4675 Scotty the Leopard

@Rosyna CGImageSourceCopyTypeIdentifiers()? Isn't Carbon deprecated in 10.5 or something?

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

Nice. Take out the focus ring on your table view.

Charles — Oct 02, 07 4700

CGImageSourceCopyTypeIdentifiers - evidently a lot of ppl have misunderstood many things. Just because it's not Cocoa - as in resident in either of the two NS/Cocoa frameworks - doesn't mean it's Carbon.

Coriander — Oct 02, 07 4701

It has been a while since I've programmed in Cocoa. But recently I cobbled together my own image and video viewer. The goal was to allow the user to drag-and-drop images and videos (like from a website) into one big, convenient canvas. In this canvas, the images and videos could be easily arranged (by dragging) and resized (using the scroll wheel). Make it smooth and responsive, and also throw in Apple Speech Recognition to control video playback, and I'd be happy. I'm more or less happy with the result. The hardest part was freely arranging and resizing the videos (and maintaining their z-order) in the canvas.

The name of the app is Coriander Collage. (As a warning, the website contains adult content.)

Blain — Oct 02, 07 4703

As a warning, the website contains adult content.

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 Scotty the Leopard

So if you guys wanted to keep the discussion on SimplePicture, that would be awesome.

Coriander — Oct 03, 07 4709

Here's a tip about images. It depends, but you can have high image quality and still keep responsiveness. Use high image quality during regular display and low image quality during live resizing. Put the following code in drawRect:

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

It's tempting to try my hand at extending SimplePicture, such as a progress indicator (blah of blah blah images) and updating the list every 100 images or so. But I'm behind schedule with Historian. I do have a question, though. In - (void)threadedReloadImageList:
[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 Scotty the Leopard

@Coriander: Use high image quality during regular display and low image quality during live resizing.
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

Here are a couple more notes.

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

I have what is probably a novice bindings question. It's not clear to me how the ImageList 'selection' binding knows how to convert the string path to an image in the custom view. Is this default behavior for this key? this part's fuzzy.

thanks!

Scott Stevenson — Oct 24, 07 4824 Scotty the Leopard

@Brian: It's not clear to me how the ImageList 'selection' binding knows how to convert the string path to an image in the custom view. Is this default behavior for this key? this part's fuzzy

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

This will really help me to (I hope) finally start developing in Cocoa !

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

Wow! I'm new to cocoa (new to Mac in general) and examples like this are very, very useful. Thank you very much!

toby — Apr 11, 08 5713

Cheers for the tips guys. Very cool that you take the time to help others like this for nix.

That skateboarding dog is awesome. I've not seen one of those before.




 

Comments Temporarily Disabled

I had to temporarily disable comments due to spam. I'll re-enable them soon.





Copyright © Scott Stevenson 2004-2015