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?

\$\endgroup\$
  • \$\begingroup\$ Have you seen this old question I posted? Look at the first code snippet: codereview.stackexchange.com/questions/56162/… \$\endgroup\$ – nhgrif Feb 25 '15 at 23:14
  • \$\begingroup\$ I will try that. \$\endgroup\$ – Pahnev Feb 25 '15 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\$ – nhgrif Feb 26 '15 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\$ – Pahnev Feb 26 '15 at 2:04
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...

\$\endgroup\$
  • \$\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\$ – Pahnev Feb 26 '15 at 3:31
  • \$\begingroup\$ I prefer keeping my categories distinct. It's not necessary though, you could combine them. \$\endgroup\$ – nhgrif Feb 26 '15 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.