Search code examples
iosswiftuiimageviewuiimageavfoundation

Take screenshot of UIImageView with AspectFill on ios


I am simply trying to capture a UIImage of a UIImageView with the contentMode set to aspectFill, but sometimes it is not working. I need it to always be of size 375 x 667 as well and perhaps the problem is associated with this but I from my testing I haven't been able to fix it :/

Here is the code used:

To Get Image:

extension UIView {

    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: CGRect(x: 0, y: 0, width: 375, height: 667))
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }

}

Usage:

//ImageView setup stuff
imgViewForVideo.image = thumbnailImage
imgViewForVideo.contentMode = .scaleAspectFill
imgViewForVideo.isHidden = false
                
let newImage = imgViewForVideo.asImage() //usage
    
UIImageWriteToSavedPhotosAlbum(newImage, self, #selector(media(_:didFinishSavingWithError:contextInfo:)), nil) //saving it to phone for testing

And here are 2 examples of what I mean: (the images should be aspect fill and fill teh entire 375 x 667 screen no matter the original UIImage size...)

Correctly aspect filled and screenshot:

enter image description here

This is an example of a mess-up: (NOTE: the black border on the left is not part of the problem that was a mistake screenshotting from my computer.. however it helps to show the white part of the screen... which is one of the problems I encounter... other than the image being way too zoomed in sometimes..)

enter image description here


Solution

  • With your current extension, you are saying:

    "Render the view at its current size in a 375 x 667 image"

    So, if your imgViewForVideo is 80 x 142 (such as showing a "thumbnail" at about the same aspect ratio), you're doing this:

    enter image description here

    What you want to do is:

    get a UIImage of the view at its current size and scale it to 375 x 667

    You can do that either by setting the frame of your imgViewForVideo to 375 x 667, or, to use the image view as-is, use this extension:

    extension UIView {
        
        // this method will work, but uses multiple image scaling operations
        // resulting in loss of image quality
        
        func resizedImage(_ size: CGSize, useScreenScale: Bool? = true) -> UIImage {
            let format = UIGraphicsImageRendererFormat()
            if useScreenScale == false {
                format.scale = 1
            }
            // use bounds of self
            var renderer = UIGraphicsImageRenderer(bounds: bounds, format: format)
            let img = renderer.image { rendererContext in
                layer.render(in: rendererContext.cgContext)
            }
            // use target size
            renderer = UIGraphicsImageRenderer(size: size, format: format)
            return renderer.image { (context) in
                img.draw(in: CGRect(origin: .zero, size: size))
            }
        }
        
    }
    

    and call it with:

        let targetSZ = CGSize(width: 375, height: 667)
        let newImage = imgViewForVideo.resizedImage(targetSZ, useScreenScale: false)
    

    Note that method ends up scaling the image multiple times, resulting in loss of quality.

    A better approach would be to use the original image and scale and crop it to your target size.

    Take a look at this extension:

    extension UIImage {
        
        // scales and clips original image
        // optionally preserving aspect ratio
        
        func scaleTo(size targetSize: CGSize, mode: UIView.ContentMode? = .scaleToFill, useScreenScale: Bool? = true) -> UIImage {
            // make sure a valid scale mode was requested
            //  if not, set it to scaleToFill
            var sMode: UIView.ContentMode = mode ?? .scaleToFill
            let validModes: [UIView.ContentMode] = [.scaleToFill, .scaleAspectFit, .scaleAspectFill]
            if !validModes.contains(sMode) {
                print("Invalid contentMode requested - using scaleToFill")
                sMode = .scaleToFill
            }
            
            var scaledImageSize = targetSize
            
            // if scaleToFill, don't maintain aspect ratio
            if mode != .scaleToFill {
                // Determine the scale factor that preserves aspect ratio
                let widthRatio = targetSize.width / size.width
                let heightRatio = targetSize.height / size.height
                
                // scaleAspectFit
                var scaleFactor = min(widthRatio, heightRatio)
                if mode == .scaleAspectFill {
                    // scaleAspectFill
                    scaleFactor = max(widthRatio, heightRatio)
                }
                
                // Compute the new image size that preserves aspect ratio
                scaledImageSize = CGSize(
                    width: size.width * scaleFactor,
                    height: size.height * scaleFactor
                )
            }
            
            // UIGraphicsImageRenderer uses screen scale, so...
            //  if targetSize is 100x100
            //      on an iPhone 8, for example, screen scale is 2
            //          renderer will produce a 750 x 1334 image
            //      on an iPhone 11 Pro, for example, screen scale is 3
            //          renderer will produce a 1125 x 2001 image
            //
            // if we want a pixel-exact image, set format.scale = 1
            let format = UIGraphicsImageRendererFormat()
            if useScreenScale == false {
                format.scale = 1
            }
            
            let renderer = UIGraphicsImageRenderer(
                size: targetSize,
                format: format
            )
            var origin = CGPoint.zero
            if mode != .scaleToFill {
                origin.x = (targetSize.width - scaledImageSize.width) * 0.5
                origin.y = (targetSize.height - scaledImageSize.height) * 0.5
            }
            let scaledImage = renderer.image { _ in
                self.draw(in: CGRect(
                    origin: origin,
                    size: scaledImageSize
                ))
            }
            
            return scaledImage
        }
        
    }
    

    Instead of calling a "convert to image" function on your image view, call scaleTo(...) directly on the image itself:

        // make sure the image view has a valid image to begin with
        guard let img = imgViewForVideo.image else {
            print("imgViewForVideo has no image !!!")
            return
        }
        
        let targetSZ = CGSize(width: 375, height: 667)
        let newImage = img.scaleTo(size: targetSZ, mode: .scaleAspectFill, useScreenScale: false)
    

    Here's an example of a 2400 x 1500 image, displayed in-app in a 80 x 142 .scaleAspectFill image view, saved out to 375 x 667, using the UIView extension:

    enter image description here

    This is the same example 2400 x 1500 image, displayed in-app in a 80 x 142 .scaleAspectFill image view, saved out to 375 x 667, using the UIImage extension:

    enter image description here

    These used this original 2400 x 1500 image:

    enter image description here

    I put an example app (that I used to generate these images) here: https://github.com/DonMag/ImageSaveExample