How to Memorize Block Syntax
Since block syntax came out alongside iOS 4, I’ve struggled to memorize it. Sure, blocks without return types or parameters = ^{ return @”super easy to recall”;}, but once you factor in arguments and want to accept a block argument in a method, thingsBecome:(CGFloat (^)(NSInteger way, id more, CGRect complex))dontYaThink?
It’s no wonder that Fucking Block Syntax has become one of my most beloved websites.
But if you’re not the Rain Man and, like me, have trouble remembering the syntax accurately, there’s hope! I have a full-proof way to memorize it.
The Two Basic Forms
There are two basic forms to memorize. The first is when your block has a name, and the second when your block is a value (anonymous):
- NSInteger (^myBlockName)(CGFloat param1)
- ^NSInteger(CGFloat param1)
Take a minute to look at these two forms. The first form is as normal and expected as could be. REMEMBER, put the block name in parenthesis. If you do that, it’s easy pickings. The ^ is like a * if the function was a pointer. Just remember those parenthesis. Visualize how the parenthesis cuddle the name of the block. Think: Cuddle The Name.
The second form is ridiculous. Backwards almost. Since there is no name associated with the block, the return type flips to the other side of the carat. No cuddling needed, either. Clearly, tho, dropping the name means we can compact the syntax. I like to remember the phrase, Flip No Cuddle. To me, we flip the side of the carat the return type is on and lose the parentheses.
Apply It Everywhere
With these two concepts in mind, the named block and the anonymous block, let’s see them all applied. Read the following sample code and identify each one, repeating the above phrases as you see each one. Recognize if the block is named or not, then say either Cuddle The Name or Flip No Cuddle:
// pass block to method [self makeNumberFromString:^NSNumber *(NSString *str) { ... }]; // define block locally NSNumber * (^local)(NSString *str) = nil; // property @property (copy, nonatomic) NSNumber * (^myProp)(NSString *str); // the defined method - (void)makeNumberFromString:(NSNumber * (^)(NSString *str))named { ... }
How did you do? Your answers should have been Flip No Cuddle, Cuddle The Name, Cuddle The Name, and WHAT THE F*** ARE YOU KIDDING ME? Yep, sorry, method parameters bend the Cuddle The Name mold a bit by flipping the method name to the outside. However, this too is easy to remember because the carat remains and, hey, ALL method parameters put their name on the outside. Right?
- (BOOL)textFieldShouldReturn:(UITextField *)textField { ... }
So, just put the whole expression in parens and the parameter name on the outside like you always do.
Typedefs? Cuddle The Name!
I’m not a huge fan of typedefs for blocks, but you’ll see them used often and you should recognize easily that defining them is Cuddle The Name:
typedef BOOL (^IsItSafeType)(NSInteger);
With it defined, assigning to it is what pattern? RIGHT! It’s Flip No Cuddle because you assign an anonymous value to the variable!
IsItSafeType alwaysSafe = ^BOOL(NSInteger x) { return YES; };
OK, Let’s Summarize!
Alright, so we have our two basic forms:
- NSInteger (^myBlockName)(CGFloat param1) <-- Cuddle The Name for named blocks
- ^NSInteger(CGFloat param1) <-- Flip No Cuddle for anonymous blocks
The one exception is with Cuddle The Name for method parameters where we cuddle the whole thing and pull the parameter name outside:
- ((NSInteger (^)(CGFloat param1))outsideName <-- Cuddle The Name wants to be like other parameters!
But as you can see, since it’s a named block, using Cuddle The Name is perfectly consistent. Then, defining typedefs, we still have a named block and so we use Cuddle The Name there, too! And assigning to the typedef? Yep. Flip No Cuddle.
Simple! Hopefully, this helps us memorize this very strange syntax. :-)
Don’t Write a Class, Write a Category!
As an iPhone freelancer, I develop a lot of iOS apps. A lot. And one of the most important things to my business is that I leverage as much of my past work as possible when it comes to new projects. I have several strategies, but today I wanted to share one of my favorites: categorization.
If you’re an experienced developer who’s worked with various languages and platforms over the years, you’ll know that the classic way to add “core” functionality was to either extend a class or create a new (probably static) class. We’d create classes like BetterString (extends String) or StringUtils as ways to add functionality to the platform we think could be handy later.
Of course, the problem with extending classes is that they’re not always interoperable with library code. Taking the example above, you might have to wrap your String in a BetterString before passing it to your code. It gets ugly. Creating new classes separates the functionality out a bit, but providing a bunch of static methods from StringUtils just feels disconnected and incorrect. (Hint: it is.)
Abuse the $*#@&^ Out of Categories!
Generally, when we think about categories, the first thing that comes to mind is adding useful NSString or UIColor methods. In InnerBand, I provide popular UIColor#-colorWithHex: and NSString#contains: methods that can be re-used all over the place without interfering with the categorized classes.
But what if I told you that using categories for app-specific functionality unlocks their true power?
You see, the best use for categories is dropping ALLLL the reusable bits you need down to the microscopic level. How much do you use categories? Answer yes or no to these questions:
- Do you specify fonts or font sizes in place when you need them?
- Do you specify colors all over your code?
- Do you access NSUserDefaults values by providing keys where you access them?
- Do you check for NSNull when you’re parsing JSON?
Did you answer yes to any of these? If so, you need to abuse categories more. You see, as a Clean Coder, it’s your mission to isolate these kinds of choices and checks as much as possible.
If you’re still reading, you want to know more. So let’s inspire you! Here’s how we can put categories to good use!
Pre-Define Your Fonts in a Category
If you’re specifying a font face or size in your view controller, you’re doing it wrong. You should define the fonts you’ll use in one place and reference them elsewhere. And we’ll use a category:
@implementation UIFont (AppFonts) + (UIFont *)titleFont { return [self bebasFontOfSize:20]; } + (UIFont *)subtitleFont { return [self bebasFontOfSize:15]; } + (UIFont *)bebasFontOfSize:(NSInteger)size { return [UIFont fontWithName:@"Bebas Neue" size:size]; } @end
As a bonus, notice how I even have a convenient private method for creating the Bebas Nueue font in case I ever need to change it.
Create Your Colors in a Category
Colors are tough in XCode. First, I DO NOT LIKE the Apple format of using decimal numbers to define them. Designers don’t use that format, it’s harder to eyeball the distributed syntax, and sooo many apps that can help you with color will output in Hex rather than Apple’s format. (If you insist on using the Apple format, I use and recommend Color Snapper.)
Past that, even if you do use hex colors, you should confine them to a category:
@implementation UIColor (AppColors) + (UIColor *)headlineColor { return [UIColor colorWithString:@"#ff0000"]; } + (UIColor *)standardBackgroundColor { return [UIColor colorWithString:@"#111111"]; } @end
Categorizing your colors in this way makes them easy to change and locates them right on the class you’ll find the most convenient. Look at the client code:
self.view.backgroundColor = [UIColor standardBackgroundColor];
Contrast that with this macro/function usage:
self.view.backgroundColor = MA_COLOR_STANDARD_BACKGROUND_COLOR();
The former just feels so natural. Naming is shorter and more readable, and I know where to find it should I need to look at it.
Don’t Re-Use the Key, Re-Use the Access
One of the more prevalent anti-patterns I’ve seen is to create a constant for use as an NSUserDefaults key. I know, my old code does it, too. :-) And yes, constants are good for this sort of thing to avoid typos, but using categories we can make saving and restoring these values even more reusable and easy to do:
// keys static NSString * const kUserDefaultsDealerId = @"kUserDefaultsDealerId"; @implementation NSUserDefaults (AppDefaults) - (void)setDealerId:(NSString *)dealerId { [self setObject:dealerId forKey:kUserDefaultsDealerId]; } - (NSString *)dealerId { return [self objectForKey:kUserDefaultsDealerId]; } @end
You can see that I use a constant for the key, but the constant and the accessors are all here on this NSUserDefaults category and, again, all very logically located. (Assuming you don’t have a separate model class.) I can read and write the value, and I do it right on the class:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setDealerId:@"ABCDE"]; [defaults synchronize];
It just feels so natural to perform these operations on the very class we intend to interact with. Notice I don’t do the synchronize in the call because there are cases where doing so would be very ineffecient, such as when you were setting the value within a loop.
Even the Lowest Level Code Can Be Categorized
I love JSON, but it can be a bit annoying to constantly check for NSNull in values you receive. (You are doing that, right?). In the old days, you may have seen a lot of code looking this:
NSString *firstName = [json valueForKey:@"first_name"]; self.firstName = (firstName && firstName != (NSString *)[NSNull null]) ? firstName : nil;
Repeated everywhere. Well, let’s categorize!
self.firstName = [json safeValueForKey:@"first_name"];
We can reuse it everywhere. What’s it look like?
@implementation NSDictionary (Nulls) - (id)safeValueForKey:(NSString *)key { id value = [self valueForKey:key]; if (value && value != [NSNull null]) { return value; } return nil; } @end
Final Thoughts
Categories are a powerful part of the Objective-C language and, for once, one that’s been there quite a while. (*cough* blocks *cough*) They get decent use in every codebase I’ve seen, but I think many stop short of truly abusing them. Hopefully, I’ve turned you into a frequent abuser today!
One last thing, I am a proponent of importing these in the PCH. Every one. Why? Well, if you take the concept of “forgotten code” as code that doesn’t get used by developers who weren’t aware the functionality was there, then the advantage of importing these in the PCH is that a new developer can more easily stumble on these without even being told. They go to define a new font and…hey, what’s that? A -headlineFont method? I think it’s a great way to help developers find their way around the codebase.
Alright, that’s all I got. Code away!
My Concise Introduction to CocoaPods
CocoaPods is now the industry standard for managing third-party frameworks and dependencies for iOS. There’s such a demand to make libraries work with CocoaPods that, even in its very early days, I actually received 5 requests to add a Podfile and tag before I even knew what CocoaPods was! Nowadays, I love it, and I’d never even consider starting a project without it!
Let’s take a concise look at how CocoaPods works.
The One-Time Installation
CocoaPods is actually implemented as a Ruby gem, so assuming you have Ruby already installed, install the gem with:
gem install cocoapods
If you’re not familiar with Ruby, you might need sudo access to get this command to work.
Setting Up Your Project
Setting up CocoaPods for your project doesn’t get much harder. First, make sure you’ve created your XCode project if you haven’t already. (I’ll wait.) Great! Now, you need to create an initial Podfile. At the top of your project hierarchy, in the same directory as your project’s .xcodeproj file, run:
pod init
This creates the Podfile for you. Now open it and add the following:
pod 'RaptureXML'
Next, install the CocoaPods and framework and your new dependency with:
pod install
Magic be done! Now take another look at your directory contents. CocoaPods has created a complementary .xcworkspace file that you should use from now on to open your project. When you do, you’ll find that your project now shares the workspace with CocoaPods and all your dependencies are managed for you!
Pod Selection and Specification
If you specified the Podfile as listed above, you’ll have the latest version of RaptureXML in your project. It’s very easy to add other libraries and be even more specific as to what version you want. Let’s say you want to add AFNetworking to your project…what do you do? I always start with a search:
pod search AFNetworking
A few items comes up, but it’s quick to see that this is what you’re looking for:
-> AFNetworking (2.0.0) A delightful iOS and OS X networking framework. pod 'AFNetworking', '~> 2.0.0' ...
Even better, the third line there is CocoaPods telling you exactly what you need to add to your Podfile to install AFNetworking! Yet, what’s that syntax? It’s a little weird, so here’s some examples to give you an idea:
pod 'AFNetworking', '~> 2.0.0' <-- Installs latest version up to, but not including, 2.1 pod 'AFNetworking', '~> 2.0' <-- Installs latest version up to, but not including, 2.9 pod 'RaptureXML', '1.0.1' <-- Installs version 1.0.1 pod 'iRate' <-- Installs most recent version ...
There’s other options in terms of versioning, but I don’t find anything else very useful. Most of the time, I just grab the latest version. There’s some risk there in case a bug is introduced into a dependency, so if it’s critical for testing that things stay the same, you might opt to lock the versions down in the release branch version of your Podfile. Stick to using the latest versions in your main branch, though, because you don’t want to become dependent on an old version of anything.
Local Pods
This is my favorite feature of CocoaPods. Previously, we’ve been installing pods from CocoaPods’ central repository, but there’s no requirement that you have to. We can alternatively load our pods directly from any Git repository:
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git'
And, my favorite, you can point directly at a path on your own machine!
pod 'AFNetworking', :path => '~/Rapture/Tilikum'
This is just brilliant because, in both of the above cases, you can manage your own pods privately without exposing them to the central repository that allows anyone access. This is great when you, or your company, have internal libraries that you want to manage with CocoaPods but don’t want anyone else using. I use this endlessly to capture re-usable code that I use repeatedly in many of my own projects.
Creating the Pod
So, how do the pods know how to configure themselves for your app? This takes a little more work, but it’s not too bad. First, you’ll likely need to re-configure your library or framework’s folder hierarchy just a little. Here are a couple tips:
- Centralize the re-usable code in your library under one directory hierarchy (i.e., Classes/ or Tilikum/).
- Separate app delegates, Info.plist, and any other files which shouldn’t be included in a host app. (So they’re not under Classes/)
Now, in your library, create your podspec at your project’s top level. Let’s pretend we’re creating a library of avatar masks. We run:
cd ~/Projects/AvatarMasker pod spec create AvatarMasker
This will create an AvatarMasker.podspec in that directory. There’s a lot of comments in that file to help you configure it. My best advice is to browse real-world podspecs to see what they do. Here are the podspecs for RaptureXML, iRate, and ZipZap.
One thing you may notice while you’re browsing around is that some projects don’t appear to have podspecs, but are searchable in CocoaPods. This is because it’s not required that the podspec be in the repository itself. In these cases, the podspec is located only on the CocoaPods central repository. This is, for example, the case for InnerBand. If you want to add your library to the central repository, you’ll just need to upload your podspec to a different place.
Also, note that if the project doesn’t host its own podspec, you can’t load it directly like we just did above. You would have to load it from the central repository.
Conclusion
CocoaPods is clearly the superior option compared to adding each dependency to your project by hand. Every library seems to have unique requirements for how to integrate it and you’ll often include more than you need when you do it this way. You’ve seen that you can integrate a library as large as AFNetworking in a matter of seconds, so why would you want to do it yourself? In addition, you gain the power to specify any version you want without having to reconfigure every time — at will.
Sometimes, developers will use Git submodules to avoid copying library source code into the app. This is fine, but personally I despise submodules. Git just doesn’t implement them very elegantly and, inevitably, you’ll run into errors when a developer doesn’t update the submodule pointer properly or forgets to push or pull submodule commits. On top of all of this, you still have to do the library integration on your own.
If you’ve never used CocoaPods, I hope I’ve inspired you to try out one of the most valuable tools in the iOS toolbox!
Skinning the UIPopoverController
The UIPopoverController was introduced in iOS 3.2 as part of the initial iPad release. An exclusive to the iPhone’s big brother, it’s seen a lot of action. Needless to say, if you’re reading this tutorial, you’ve probably already used the UIPopoverController dozens of times! But, have you ever needed to change its appearance?
In this tutorial, I’ll show you how.
Skinning the Popover
The standard look of a UIPopoverController has always been a nice one, and for a long time it’s the only one you’d see because there was no way to change it. If you wanted it to have a different appearance, you were forced to write your own! Thankfully, when iOS 5 made landfall, the -popoverBackgroundViewClass property was introduced and you could finally, natively, skin the popover any way you liked!
Unfortunately, the interface can be somewhat confusing and demand a lot of logic from a developer. Let’s walk through a custom skin and explain how to get it to work. The source code for this tutorial is here.
Step 1: Create the Assets
You need to create two assets for a popover skin: the background image and the arrow image. These are straightforward once you see an example. Let’s look at the “orange theme” we’ll be implementing:
Pretty simple, right? You can go with as complex a design as you’d like, however, it doesn’t really get any more complicated. Also, note the squared edges. We’ll actually round them in code, but know that you can also do it right in the graphics asset if you wanted to.
Step 2: Subclass UIPopoverBackgroundView
Your skin class must extend the abstract UIPopoverBackgroundView class. There’s a series of methods you’ll need to define, so let’s walk through each one and add them to our class.
Step 3: Implement +contentViewInsets
Imagine the background of your skin, minus the arrow. The content of your popover will use all of this space when presented to the user. The insets define the margins we use around it. This allows us to see the boundaries of the popover much like the default skin. Our skin will inset 10 points all around:
+ (UIEdgeInsets)contentViewInsets { return UIEdgeInsetsMake(10, 10, 10, 10); }
Step 4: Implement +arrowBase and +arrowHeight
These two static methods merely define the size of your arrow. Putting it simply, you can just return the asset’s width and height dimensions. If you load the arrow image statically (in +initialize), you can just return the frame’s width and height. We’ll use constants:
+ (CGFloat)arrowBase { return 31.0; } + (CGFloat)arrowHeight { return 70.0; }
Note that the arrow image is rectangular. A square image is a little more forgiving if you get some of the layout math incorrect, but I wanted to demonstrate things with the rectangular arrow appearance.
Step 5: Implement +wantsDefaultContentAppearance
This property allows you to skip some pretty subtle effects Apple adds to the popover’s appearance. Change this value to see what your popup looks like in both modes.
+ (BOOL)wantsDefaultContentAppearance { return YES; }
Step 6: Implement the arrowDirection property
These next two steps are just adding properties. The SDK will supply us with the arrow direction for our popover at some point before displaying it. It will be one of the four basic directions: up, down, left, or right. The user can specify the arrow to be in “any” direction as well, but that value will be calculated and the rest is what will be set here. Once iOS determines which direction it should use of the four, it sets this property. All you need to remember is to ask the view to re-layout itself after we set it.
- (void)setArrowDirection:(UIPopoverArrowDirection)arrowDirection { _arrowDirection = arrowDirection; [self setNeedsLayout]; } - (UIPopoverArrowDirection)arrowDirection { return _arrowDirection; }
Step 7: Implement the arrowOffset property
At first glance, arrowOffset is a mysterious value. What is it? Much of the time, the value will be 0.0, or no offset. What it’s used for is when you present a popover near the edge of the screen and, since the popover arrow normally wants to horizontally or vertically aligned with the point it’s presented from, has to adjust. Visually, this will mean the arrow will seem to “slide” along the bubble’s axises.
We save the value and use it later when we need to lay out the views.
- (void)setArrowOffset:(CGFloat)arrowOffset { _arrowOffset = arrowOffset; [self setNeedsLayout]; } - (CGFloat)arrowOffset { return _arrowOffset; }
Step 9: Initialize Our Skin
We’ll back up here for a second and properly initialize our skin for customization. This generally involves two things: instantiate the background and arrow image views. You’ll see that we also want to make sure the background image is properly resizable since we want to handle any size popup. (For our skin, it’s not necessary, but the code is presented anyway.) Another interesting thing we’ll do is round the corners of our popover similarly to the original. We could just have easily done this in the image as well, so if you prefer to do it like that, go for it.
- (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { // background (for demo only, we don't actually need this for our background) UIImage *backgroundImage = [UIImage imageNamed:@"bubble-rect"]; UIImage *resizableBackgroundImage = [backgroundImage resizableImageWithCapInsets:UIEdgeInsetsMake(40, 40, 40, 40) resizingMode:UIImageResizingModeStretch]; _backgroundImageView = [[UIImageView alloc] initWithImage:resizableBackgroundImage]; _backgroundImageView.contentMode = UIViewContentModeScaleToFill; // round those corners! _backgroundImageView.layer.cornerRadius = 15; _backgroundImageView.layer.masksToBounds = YES; // arrow UIImage *arrowImage = [UIImage imageNamed:@"bubble-triangle"]; _arrowImageView = [[UIImageView alloc] initWithImage:arrowImage]; // make sure the arrow is on top of the background [self addSubview:_backgroundImageView]; [self addSubview:_arrowImageView]; } return self; }
Make sure the arrow’s z-order is above the background’s. You’ll find it’s always going to be easier to keep a clean seam where the two meet by doing it this way.
Step 10: Implement -layoutSubviews
Finally, with the formalities out of the way, here’s where we earn our bacon. We know the arrow size and all the properties of the popover. The system is ready to render it, and now here is where we have to properly size everything according to plan.
In the -layoutSubviews method, we’ll need to do the following:
- Rotate the arrow correctly.
- Position the arrow correctly.
- Size the background to accomodate the arrow’s position.
The math can get tricky here. This method is completely reusable if you’re wanting the standard popover behavior that we’ve all come to expect. Unless you’re doing something radically different, this is all you’ll need.
- (void)layoutSubviews { [super layoutSubviews]; CGFloat arrowHeight = [self.class arrowHeight]; CGRect backgroundFrame = self.frame; CGPoint arrowCenter = CGPointZero; CGFloat arrowTransformInRadians = 0; if (self.arrowDirection == UIPopoverArrowDirectionUp) { backgroundFrame.origin.y += arrowHeight; backgroundFrame.size.height -= arrowHeight; arrowTransformInRadians = 0; arrowCenter = CGPointMake(backgroundFrame.size.width * 0.5 + self.arrowOffset, arrowHeight * 0.5); } else if (self.arrowDirection == UIPopoverArrowDirectionDown) { backgroundFrame.size.height -= arrowHeight; arrowTransformInRadians = M_PI; arrowCenter = CGPointMake(backgroundFrame.size.width * 0.5 + self.arrowOffset, backgroundFrame.size.height + arrowHeight * 0.5); } else if (self.arrowDirection == UIPopoverArrowDirectionLeft) { backgroundFrame.origin.x += arrowHeight; backgroundFrame.size.width -= arrowHeight; arrowTransformInRadians = M_PI_2 * 3.0; arrowCenter = CGPointMake(arrowHeight * 0.5, backgroundFrame.size.height * 0.5 + self.arrowOffset); } else if (self.arrowDirection == UIPopoverArrowDirectionRight) { backgroundFrame.size.width -= arrowHeight; arrowTransformInRadians = M_PI_2; arrowCenter = CGPointMake(backgroundFrame.size.width + arrowHeight * 0.5, backgroundFrame.size.height * 0.5 + self.arrowOffset); } _backgroundImageView.frame = backgroundFrame; _arrowImageView.center = arrowCenter; _arrowImageView.transform = CGAffineTransformMakeRotation(arrowTransformInRadians); }
Your frame of mind here should be that your popover has been pre-sized to a rectangle which includes the popover background and the space the arrow takes up. Based on the arrow position, you want to constrict the rectangle and then position your arrow image where it needs to be. If you simply accepted the pre-sized frame as is, you could make a popover with no arrow. I don’t recommend it, but you could.
As a personal preference, I like to render my arrow assets in the up direction and rotate as needed from there. Like I said, this is just personal. Whatever is easiest to visualize for you, just make sure you rotate it properly as we did here.
Another interesting point is that my code never uses the +arrowBase value. I’m sure the SDK does, so we still have to provide it, of course. But it’s not strictly necessary to render properly in our own code.
Conclusion
Here’s our themed popover!
Skinning a popover is not the most intuitive experience Apple has provided for its developers, but as you can see once you implement it correctly, you can re-use it again and again. I’m not sure why Apple made a generic skinning framework for this when the popover API itself seems to really enforce a narrow view of what a popover is. However, now that we have the power to customize our popovers, it’s good to know we can provide an algorithm pretty easily.
Asset Catalogs: Love Em or Leave Em?
I’ve never been to a WWDC, but each year I get positively gitty when the session videos become available. And what do I get the most excited about? No, not Passbook, iCloud, or the big-ticket stuff. No, I like the small stuff. The simple stuff. The stuff that improves my day-to-day and fixes things I constantly find annoying.
This year, that something was Asset Catalogs.
Why Asset Catalogs? Well, simple. Ever since we got the iPhone 4 in all it’s retina glory, I’ve always found it frustrating to manage keeping TWO versions of every graphic. (The original and it’s double-resolution version.) There’s so much monotony in it:
- I need to keep all the files paired together in Xcode, in the right order. (Partly due to OCD, partly due to basic organization.)
- I need to make sure they’re both named EXACTLY the same.
- If I want to rename an image, I GOTTA DO IT TWICE.
- I hate how much space it takes up on the Project Navigator when the folders inevitably all expand.
- I need to make sure the @2x is exactly twice the dimensions of the original. (Designers always flub this.)
Sadly, Asset Catalogs don’t fix #5, but they do help us a ton with the other four, and hey, that ain’t bad! Still, not all is rosy in a catalogued world, so let’s take a look, shall we?
Asset Catalogs: The Good
No longer do you have to bother keeping your images paired next to each other in XCode (you *do* that, right?) Asset Catalogs allow you to specify retina and non-retina versions together in one spot.
Now, you simply drag your images into the the catalog and then drop their countertops right alongside them. They’re still files, too, just right-click and reveal them in Finder to see. This is lovely! It also alleviates the need to keep the naming in sync since they share one name.
Even nicer, you no longer have to manage the naming of fifty-billion Icon images for all the different device models and iOS versions Apple has out there. (And there’s no single standard.) You simply specify to use your Asset Catalog and it’ll find all the right sizes for you.
As an extra bonus, it’s much easier to browse through all your images and make sure you have both versions! This is really nice for older projects that pre-dated retina, though you’ll still have to migrate to asset catalogs first. In the old days, verifying the retina images involved a sharp eye as you scrolled through lists of filenames.
Hooray!
Asset Catalogs: The Bad
Bad? How can they be bad? We’ve already seen how Asset Catalogs keep the image files organized, correctly named, and easily viewable. What more could you add?
Well, there’s something that we lost. You can no longer Command-Shift-O to look for an image. Try it. Now, why would you want to do that? Well, sometimes I’m crawling through code and I find code loading an image:
UIImage *buttonImage = [UIImage imageNamed:@"btn_clear"];
Hmm, what is that image, let’s Command-Shift-O and…oh. No image. I found this REALLY useful before. Now, if you want to search for an image, you need to open your Asset Catalog and do your search in the field in the bottom left corner.
It’s not hard, but it’s bulky and involves multiple steps. I really hope they add this to the indexer someday. And one more thing, and if you’re familiar with this search then you know where I’m going, have you noticed the search bug? Yes, hopefully it’s temporary, but Xcode will often persist your search forever. Got Xcode in front of you? Enter an asset search, clear it, tap another file in the Project Navigator, then tap the Asset Catalog again.
HOORAY! You’re locked into your search again! Ugg. Fix it, Apple!
One Final Quirk
Another little annoyance to Asset Catalogs, that hopefully will change in the future, is that Test Flight still wants to see an Icon.png file in your uploaded apps. The only way around it is to include a separate icon image in Xcode and add an icon entry back into the plist.
I’m sure this will get cured in the future as well, but for now if you use TestFlight you should keep a regular icon file around.