5
\$\begingroup\$

I am trying to create a class that takes a screenshot of the screen, makes it blurry and saves it to NSUserDefaults.

- (UIImage*)blurImage
{
 UIImage* newImage = [[UIImage screenshot] applyBlurWithRadius:20
 blurType:BOXFILTER
 tintColor:[UIColor colorWithWhite:0.11f alpha:0.5]
 saturationDeltaFactor:1.3
 maskImage:nil];
 return newImage;
}
- (void)saveBlurredImageToUserDefaults
{
 [[NSUserDefaults standardUserDefaults] setObject:UIImagePNGRepresentation([self blurImage]) forKey:@"blurredImage"];
 [[NSUserDefaults standardUserDefaults] synchronize];
}

applyBlurWithRadius:blurType:tintColor:saturationDeltaFactor:maskImage: is a UIImage category based on code provided by Apple at WWDC 2013. that can be found here in full.

Here is that method in case that repo disappears:

- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius blurType: (BlurType) blurType tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage
{
 // Check pre-conditions.
 if (self.size.width < 1 || self.size.height < 1) {
 NSLog (@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self);
 return nil;
 }
 if (!self.CGImage) {
 NSLog (@"*** error: image must be backed by a CGImage: %@", self);
 return nil;
 }
 if (maskImage && !maskImage.CGImage) {
 NSLog (@"*** error: maskImage must be backed by a CGImage: %@", maskImage);
 return nil;
 }
 CGRect imageRect = { CGPointZero, self.size };
 UIImage *effectImage = self;
 BOOL hasBlur = blurRadius > __FLT_EPSILON__ && blurType != NOBLUR;
 BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__;
 if (hasBlur || hasSaturationChange) {
 UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
 CGContextRef effectInContext = UIGraphicsGetCurrentContext();
 CGContextScaleCTM(effectInContext, 1.0, -1.0);
 CGContextTranslateCTM(effectInContext, 0, -self.size.height);
 CGContextDrawImage(effectInContext, imageRect, self.CGImage);
 vImage_Buffer effectInBuffer;
 effectInBuffer.data = CGBitmapContextGetData(effectInContext);
 effectInBuffer.width = CGBitmapContextGetWidth(effectInContext);
 effectInBuffer.height = CGBitmapContextGetHeight(effectInContext);
 effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext);
 UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
 CGContextRef effectOutContext = UIGraphicsGetCurrentContext();
 vImage_Buffer effectOutBuffer;
 effectOutBuffer.data = CGBitmapContextGetData(effectOutContext);
 effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext);
 effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext);
 effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext);
 if (hasBlur) {
 // A description of how to compute the box kernel width from the Gaussian
 // radius (aka standard deviation) appears in the SVG spec:
 // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
 // 
 // For larger values of 's' (s >= 2.0), an approximation can be used: Three
 // successive box-blurs build a piece-wise quadratic convolution kernel, which
 // approximates the Gaussian kernel to within roughly 3%.
 //
 // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
 // 
 // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
 // 
 CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale];
 uint32_t radius = floor(inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5);
 if (radius % 2 != 1) {
 radius += 1; // force radius to be odd so that the three box-blur methodology works.
 }
 if (blurType == BOXFILTER)
 {
 vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
 vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
 vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
 }
 else
 {
 vImageTentConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
 }
 }
 BOOL effectImageBuffersAreSwapped = NO;
 if (hasSaturationChange) {
 CGFloat s = saturationDeltaFactor;
 CGFloat floatingPointSaturationMatrix[] = {
 0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0,
 0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0,
 0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0,
 0, 0, 0, 1,
 };
 const int32_t divisor = 256;
 NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]);
 int16_t saturationMatrix[matrixSize];
 for (NSUInteger i = 0; i < matrixSize; ++i) {
 saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor);
 }
 if (hasBlur) {
 vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
 effectImageBuffersAreSwapped = YES;
 }
 else {
 vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
 }
 }
 if (!effectImageBuffersAreSwapped)
 effectImage = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 if (effectImageBuffersAreSwapped)
 effectImage = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 }
 // Set up output context.
 UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
 CGContextRef outputContext = UIGraphicsGetCurrentContext();
 CGContextScaleCTM(outputContext, 1.0, -1.0);
 CGContextTranslateCTM(outputContext, 0, -self.size.height);
 // Draw base image.
 CGContextDrawImage(outputContext, imageRect, self.CGImage);
 // Draw effect image.
 if (hasBlur) {
 CGContextSaveGState(outputContext);
 if (maskImage) {
 CGContextClipToMask(outputContext, imageRect, maskImage.CGImage);
 }
 CGContextDrawImage(outputContext, imageRect, effectImage.CGImage);
 CGContextRestoreGState(outputContext);
 }
 // Add in color tint.
 if (tintColor) {
 CGContextSaveGState(outputContext);
 CGContextSetFillColorWithColor(outputContext, tintColor.CGColor);
 CGContextFillRect(outputContext, imageRect);
 CGContextRestoreGState(outputContext);
 }
 // Output image is ready.
 UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 return outputImage;
}

Now calling this takes only one line (+ alloc&init), which is pretty good to me, but is there anything else I should do?

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Feb 25, 2015 at 23:05
\$\endgroup\$
4
  • \$\begingroup\$ Have you seen this old question I posted? Look at the first code snippet: codereview.stackexchange.com/questions/56162/… \$\endgroup\$ Commented Feb 25, 2015 at 23:14
  • \$\begingroup\$ I will try that. \$\endgroup\$ Commented Feb 25, 2015 at 23:19
  • \$\begingroup\$ What is applyBlurWithRadius:blurType:tintColor:saturationDeltaFactor:maskImage:? I can't find this method and it's not in the UIImage documentation. \$\endgroup\$ Commented Feb 26, 2015 at 1:10
  • \$\begingroup\$ I added that method to the question. Wasn't sure if should add the whole category or just that method. \$\endgroup\$ Commented Feb 26, 2015 at 2:04

1 Answer 1

4
\$\begingroup\$

Ultimately, it looks like your code has gone a long way toward being very convenient in the one spot you want to save a blurred screenshot to NSUserDefaults... but a long way toward being highly specific and not at all reusable.


For starters, we don't need a class for this at all. Classes should be thought of as things... as nouns... as the subject of a sentence. Methods, meanwhile, should be thought of as actions... as verbs... as the predicate of a sentence. We should create a class when we need to represent a thing. We should create a method when we need to do an action. We should not create a class simply because we feel like we need something to hold our collection of methods.

Objective-C is a strict superset of C. Anything you can do in C can also be done in Objective-C. And in C, you don't even have classes! So you certainly weren't creating classes just for the sake of holding a collection of methods! But like in C, in Objective-C, we can create "free functions". Functions that do not belong to any classes.

Also, importantly, in Objective-C, when we need to add actions to an existing thing, we can do that too!

If we keep with the grammar metaphor, the properties of a class are sort of like adjectives. If we need to add adjectives to a class, we should create a subclass. If all we need to do is add actions (verbs... methods...) to a class, then in Objective-C, we can create a category.

Your code makes use of two good examples of using categories, so I'm not sure why your blurImage method is anything but a UIImage category and why you've created a class?

So, let's make it a category:

UIImage+Blur.h

@interface UIImage (Blur)
- (UIImage *)blurImage;
@end

UIImage+Blur.m

@implementation UIImage (Blur)
- (UIImage *)blurImage {
 return [self applyBlurWithRadius:20
 blurType:BOXFILTER
 tintColor:[UIColor colorWithWhite:0.11f alpha:0.5]
 saturationDeltaFactor:1.3
 maskImage:nil];
}
@end

Though, a more specific name for this specific type of blur would probably be appropriate. You may decide you want to create other convenience methods for other types of blurs.

Now, we can get a blurred screenshot as simple as:

UIImage *blurredScreenshot = [[UIImage screenshot] blurImage];

And if we're really doing this so frequently, we can get rid of the nesting by adding another method to our category:

+ (UIImage *)blurredScreenshot {
 return [[UIImage screenshot] blurImage];
}

And then:

UIImage *blurredScreenshot = [UIImage blurredScreenshot];

The advantage here is that we've made it more convenient for us (created the blurredScreenshot method), but we haven't hampered the reusability, because we still have the screenshot and blurImage methods. These methods do distinct things and anyone may want to use these individually at a later date (including yourself).


As far as saving to NSUserDefaults? This is already a one-liner. Calling synchronize is almost always unnecessary and doing so can have a negative impact on your app's performance. The only time you will lose data via not synchronized is if your app crashes or possibly if the user's device crashes. If the former is a problem, calling synchronize isn't really the real fix, is it? If the latter is the problem... I wouldn't bother with it... and tell the user to get their device fixed...


And finally, one-lining things isn't always inherently good. Code should be readable, not terse. Fewer lines doesn't make code run faster, meanwhile, it does generally make human reading and debugging more difficult.

Also, keys should typically be defined as a constant somewhere... I usually have a header file:

NSUserDefaultsKeys.h

NSString * const kUSERDEFKEY_BLURREDIMAGE = @"blurredImage";

So, with the given category above, I'd most likely implement it as such:

UIImage *blurredScreenshot = [[UIImage screenshot] blurImage];
NSData *blurredJPEGdata = UIImagePNGRepresentation(blurredScreenshot);
[[NSUserDefaults standardDefaults] setObject:blurredJPEGdata 
 forKey:kUSERDEFKEY_BLURREDIMAGE];

And I wouldn't necessarily try wrapping this up into any sort of convenience method or function. This is just fine as is...

answered Feb 26, 2015 at 2:42
\$\endgroup\$
2
  • \$\begingroup\$ I've read many of your answers and as always very detailed and well explained. Thank you! So saving to NSUserDefaultsshould always be called only when needed? Thank you for explaining category vs subclass. I was trying to figure out what to use and when, but now I actually get it. One question about categories though. Is it better to have one category to do one thing or is it ok to combine them? Now if I add that blur category I will have 3 UIImage categories. \$\endgroup\$ Commented Feb 26, 2015 at 3:31
  • \$\begingroup\$ I prefer keeping my categories distinct. It's not necessary though, you could combine them. \$\endgroup\$ Commented Feb 26, 2015 at 14:29

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.