Search code examples
iosswiftuigraphicscontextuigraphicsimagerenderer

How to scale crop and cut the UIImage to a circle in Swift using UIGraphicsImageRenderer?


Currently i am using the following swift code for scaling cropping and making a circular image out of the passed UIImage . Everything is working until iOS 18 but after that i can observe crashes . I found that from iOS 18 onwards the following three methods have been deprecated;

UIGraphics.BeginImageContextWithOptions(size, false, ScreenDensity);
var image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();

From other SO posts it is recommended to use the you would use UIGraphicsImageRenderer and its associated functions. Here is the full code using the above deprecated APIs.

func scaleCropAndCircleImage(image:UIImage, frame:CGRect) throws -> UIImage {
    
    enum FunctionError: Error {
        case nilContext
        case nilImage
    }
    
    
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(frame.size.width, frame.size.height), false, 0.0)
    guard let context = UIGraphicsGetCurrentContext() else { throw FunctionError.nilContext }
    
    // width and heights of passed values of uiimage and CGRect
    let passedimageWidth = image.size.width
    let passedimageHeight = image.size.height
    let passedrectWidth = frame.size.width
    let passedrectHeight = frame.size.height
    
    // scale factor calculation
    let scaleFactorX = passedrectWidth/passedimageWidth
    let scaleFactorY = passedrectHeight/passedimageHeight
    
    // centre of the circle calculation
    let imageCentreX = passedrectWidth/2
    let imageCentreY = passedrectHeight/2
    
    // Creating and clipping to a CIRCULAR Path
    let radius = passedrectWidth/2
    context.beginPath()
    context.addArc(center: CGPoint(x: imageCentreX, y: imageCentreY), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: false)
    context.closePath()
    context.clip()
    
    //Seting  the scaling factor for graphics context
    context.scaleBy(x: scaleFactorX, y: scaleFactorY)
    

    let myRect:CGRect = CGRectMake(0, 0, passedimageWidth, passedimageHeight)
    image.draw(in: myRect)
    
    let newCroppedImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    guard let newCroppedImage else { throw FunctionError.nilImage }
    
    return newCroppedImage
}

Now i am trying to replace the deprecated functions by using the UIGraphicsImageRenderer but i am stuck in the mid way how to convert the already used logic for the UIGraphicsImageRenderer functions . Below is the code i have tried so far. Can someone help me in this so that the below function can accept the UIImage and crop it into a circle as done in existing code?

import UIKit

extension UIImage {
    func roundedCornerImage(with radius: CGFloat) -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        format.scale = scale
        let renderer = UIGraphicsImageRenderer(size: size, format: format)
        return renderer.image { rendererContext in
            let rect = CGRect(origin: .zero, size: size)
            let path = UIBezierPath(roundedRect: rect,
                                    byRoundingCorners: .allCorners,
                                    cornerRadii: CGSize(width: radius, height: radius))
            path.close()

            let cgContext = rendererContext.cgContext
            cgContext.saveGState()
            path.addClip()
            draw(in: rect)
            cgContext.restoreGState()
        }
    }
}

Solution

  • If you are just clipping a circle there is no need to calculate any corner size. Just use an eclipse to clip your image.

    extension UIImage {
        func scaleCropAndCircleImage(to length: CGFloat) -> UIImage {
            let format = UIGraphicsImageRendererFormat()
            format.scale = scale
            let size = CGSize(width: length, height: length)
            return UIGraphicsImageRenderer(
                size: size,
                format: format
            ).image {
                let rect: CGRect = .init(origin: .zero, size: size)
                $0.cgContext.addEllipse(in: rect)
                $0.cgContext.clip()
                draw(in: rect)
            }
        }
    }
    

    Playground test

    Task {
        do {
            let url = URL(string: "https://i.sstatic.net/Xs4RX.jpg")!
            let data = try await URLSession.shared.data(from: url).0
            if let image = UIImage(data: data) {
                let result = image.scaleCropAndCircleImage(to: 100)
                // use the resulting image here
            }
        } catch {
            print(error)
        }
    }
    

    enter image description here

    Note that this assumes you are using a squared image. If your images might not be squared you would need to check if it is portrait or landscape and crop what's exceeding horizontally or vertically similar to what's been done in here Cut a UIImage into a circle:

    extension UIImage {
        var isPortrait:  Bool    { size.height > size.width }
        var isLandscape: Bool    { size.width > size.height }
        var breadth:     CGFloat { min(size.width, size.height) }
        var breadthSize: CGSize  { .init(width: breadth, height: breadth) }
        var breadthRect: CGRect  { .init(origin: .zero, size: breadthSize) }
        func scaledAndCircleCroped(to length: CGFloat) -> UIImage? {
            guard let squared = cgImage?.cropping(
                to: .init(
                    origin: .init(
                        x: isLandscape ? ((size.width-size.height)/2).rounded(.down) : 0,
                        y: isPortrait  ? ((size.height-size.width)/2).rounded(.down) : 0),
                        size: breadthSize
                )
            ) else { return nil }
            let format = imageRendererFormat
            format.opaque = false
            let squaredImage = UIImage(
                cgImage: squared,
                scale: format.scale,
                orientation: imageOrientation
            )
            let size: CGSize = .init(width: length, height: length)
            return UIGraphicsImageRenderer(size: size, format: format)
                .image {
                let rect: CGRect = .init(origin: .zero, size: size)
                    $0.cgContext.addEllipse(in: rect)
                    $0.cgContext.clip()
                    squaredImage.draw(in: rect)
            }
        }
    }
    

    Usage:

    Task {
        do {
            let url = URL(string: "https://i.sstatic.net/Xs4RX.jpg")!
            let data = try await URLSession.shared.data(from: url).0
            if let result = UIImage(data: data)?.scaledAndCircleCroped(to: 100) {
                // use your resulting image here
            }
        } catch {
            print(error)
        }
    }