15

It is pretty easy to add border to UIImageView, using layers (borderWidth, borderColor etc.). Is there any possibility to add border to image, not to image view? Does somebody know?

Update:

I tried to follow the suggestion below und used extension. Thank you for that but I did not get the desired result. Here is my code. What is wrong?

import UIKit
    class ViewController: UIViewController {

    var imageView: UIImageView!
    var sizeW = CGFloat()
    var sizeH = CGFloat()

    override func viewDidLoad() {
        super.viewDidLoad()

        sizeW = view.frame.width
        sizeH = view.frame.height

        setImage()
    }

    func setImage(){

        //add image view
        imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: sizeW/2, height: sizeH/2))
        imageView.center = view.center
        imageView.tintColor = UIColor.orange
        imageView.contentMode = UIViewContentMode.scaleAspectFit

        let imgOriginal = UIImage(named: "plum")!.withRenderingMode(.alwaysTemplate)
        let borderImage = imgOriginal.imageWithBorder(width: 2, color: UIColor.blue)
        imageView.image = borderImage

        view.addSubview(imageView)

    }



}


    extension UIImage {
    func imageWithBorder(width: CGFloat, color: UIColor) -> UIImage? {
        let square = CGSize(width: min(size.width, size.height) + width * 2, height: min(size.width, size.height) + width * 2)
        let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
        imageView.contentMode = .center
        imageView.image = self
        imageView.layer.borderWidth = width
        imageView.layer.borderColor = color.cgColor
        UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        imageView.layer.render(in: context)
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result
    }
}

The second image with the red border is more or less what I need:

enter image description here

enter image description here

Roman
  • 759
  • 2
  • 9
  • 23

4 Answers4

22

Strongly inspired by @herme5, refactored into more compact Swift 5/iOS12+ code as follows (fixed vertical flip issue as well):

public extension UIImage {

    /**
    Returns the flat colorized version of the image, or self when something was wrong

    - Parameters:
        - color: The colors to user. By defaut, uses the ``UIColor.white`

    - Returns: the flat colorized version of the image, or the self if something was wrong
    */
    func colorized(with color: UIColor = .white) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(size, false, scale)

        defer {
            UIGraphicsEndImageContext()
        }

        guard let context = UIGraphicsGetCurrentContext(), let cgImage = cgImage else { return self }


        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)

        color.setFill()
        context.translateBy(x: 0, y: size.height)
        context.scaleBy(x: 1.0, y: -1.0)
        context.clip(to: rect, mask: cgImage)
        context.fill(rect)

        guard let colored = UIGraphicsGetImageFromCurrentImageContext() else { return self }

        return colored
    }

    /**
    Returns the stroked version of the fransparent image with the given stroke color and the thickness.

    - Parameters:
        - color: The colors to user. By defaut, uses the ``UIColor.white`
        - thickness: the thickness of the border. Default to `2`
        - quality: The number of degrees (out of 360): the smaller the best, but the slower. Defaults to `10`.

    - Returns: the stroked version of the image, or self if something was wrong
    */

    func stroked(with color: UIColor = .white, thickness: CGFloat = 2, quality: CGFloat = 10) -> UIImage {

        guard let cgImage = cgImage else { return self }

        // Colorize the stroke image to reflect border color
        let strokeImage = colorized(with: color)

        guard let strokeCGImage = strokeImage.cgImage else { return self }

        /// Rendering quality of the stroke
        let step = quality == 0 ? 10 : abs(quality)

        let oldRect = CGRect(x: thickness, y: thickness, width: size.width, height: size.height).integral
        let newSize = CGSize(width: size.width + 2 * thickness, height: size.height + 2 * thickness)
        let translationVector = CGPoint(x: thickness, y: 0)


        UIGraphicsBeginImageContextWithOptions(newSize, false, scale)

        guard let context = UIGraphicsGetCurrentContext() else { return self }

        defer {
            UIGraphicsEndImageContext()
        }
        context.translateBy(x: 0, y: newSize.height)
        context.scaleBy(x: 1.0, y: -1.0)
        context.interpolationQuality = .high

        for angle: CGFloat in stride(from: 0, to: 360, by: step) {
            let vector = translationVector.rotated(around: .zero, byDegrees: angle)
            let transform = CGAffineTransform(translationX: vector.x, y: vector.y)

            context.concatenate(transform)

            context.draw(strokeCGImage, in: oldRect)

            let resetTransform = CGAffineTransform(translationX: -vector.x, y: -vector.y)
            context.concatenate(resetTransform)
        }

        context.draw(cgImage, in: oldRect)

        guard let stroked = UIGraphicsGetImageFromCurrentImageContext() else { return self }

        return stroked
    }
}


extension CGPoint {
    /** 
    Rotates the point from the center `origin` by `byDegrees` degrees along the Z axis.

    - Parameters:
        - origin: The center of he rotation;
        - byDegrees: Amount of degrees to rotate around the Z axis.

    - Returns: The rotated point.
    */
    func rotated(around origin: CGPoint, byDegrees: CGFloat) -> CGPoint {
        let dx = x - origin.x
        let dy = y - origin.y
        let radius = sqrt(dx * dx + dy * dy)
        let azimuth = atan2(dy, dx) // in radians
        let newAzimuth = azimuth + byDegrees * .pi / 180.0 // to radians
        let x = origin.x + radius * cos(newAzimuth)
        let y = origin.y + radius * sin(newAzimuth)
        return CGPoint(x: x, y: y)
    }
}

Result obtained for image.stroked()

Husam
  • 8,149
  • 3
  • 38
  • 45
Stéphane de Luca
  • 12,745
  • 9
  • 57
  • 95
  • 1
    You forgot to include the `CGPoint.rotated` func. – David May 10 '20 at 00:29
  • 1
    Sure. I wanted to try your code for outlining country flags but the outline at the top is missing. Basically, after calling `stroked` the `UIImage` has an outline at the bottom and at the sides. Any clue as to why this might be happening? I'm using it on a vanilla UIImageView with flags urls taken from wikipedia. – David May 10 '20 at 01:00
  • If anyone is looking for something simpler (rectangular `UIImage` / simple borders) [these answers](https://stackoverflow.com/questions/1354811/how-can-i-take-an-uiimage-and-give-it-a-black-border) might be sufficient. – David May 10 '20 at 01:03
  • This was so helpful than you so much! However, this line: context.translateBy(x: 0, y: size.height) Should be: context.translateBy(x: 0, y: newSize.height) – bymannan May 31 '20 at 11:54
  • 1
    Great solution, I've suggested an edit which fixes a centering issue after translation. Please approve it – Husam Jun 03 '20 at 15:01
  • Thanks Husam, thanks to have pointed that. Wrongily, the addition wasn't included in my original post (copy/paste issue). So thanks again for the fix! – Stéphane de Luca Jun 03 '20 at 19:07
  • Btw, how can I approve the fix? I don't see any cal to action for that @Husam? – Stéphane de Luca Jun 03 '20 at 19:09
  • 1
    @StéphanedeLuca, I didn't notice that my edits doesn't need a peer approval. BTW you have changed the wrong `size` object in ur last edit, I've edited it again, you can check the history of edits, Everything should be fine now. BR. – Husam Jun 03 '20 at 22:12
  • Can anyone translate to ObjC pls? – GeneCode Oct 31 '20 at 02:55
  • For me, it's not doing much. If I increase the thickness, it's setting a boundary on the inside edge of the image. – Tripti Kumar Nov 18 '20 at 17:43
  • 1
    Great!! I googled a lot and your way is the best solution! – oOEric Mar 15 '21 at 07:43
  • Anyone got a version that works with `NSImage` for macOS? – hotdogsoup.nl Apr 01 '22 at 11:34
  • 1
    Thank you man this worked perfectly for my icons – Esko918 Apr 19 '22 at 20:44
  • This solution didn't work for me as the image it returned was rotated and stretched. I wound up using this solution instead: https://stackoverflow.com/a/61552479 – Smartcat Jan 12 '23 at 23:24
13

Here is a UIImage extension I wrote in Swift 4. As IOSDealBreaker said this is all about image processing, and some particular cases may occur. You should have a png image with a transparent background, and manage the size if larger than the original.

  • First get a colorised "shade" version of your image.
  • Then draw and redraw the shade image all around a given origin point (In our case around (0,0) at a distance that is the border thickness)
  • Draw your source image at the origin point so that it appears on the foreground.
  • You may have to enlarge your image if the borders go out of the original rect.

My method uses a lot of util methods and class extensions. Here is some maths to rotate a vector (which is actually a point) around another point: Rotating a CGPoint around another CGPoint

extension CGPoint {
    func rotated(around origin: CGPoint, byDegrees: CGFloat) -> CGPoint {
        let dx = self.x - origin.x
        let dy = self.y - origin.y
        let radius = sqrt(dx * dx + dy * dy)
        let azimuth = atan2(dy, dx) // in radians
        let newAzimuth = azimuth + (byDegrees * CGFloat.pi / 180.0) // convert it to radians
        let x = origin.x + radius * cos(newAzimuth)
        let y = origin.y + radius * sin(newAzimuth)
        return CGPoint(x: x, y: y)
    }
}

I wrote my custom CIFilter to colorise an image which have a transparent background: Colorize a UIImage in Swift

class ColorFilter: CIFilter {
    var inputImage: CIImage?
    var inputColor: CIColor?
    private let kernel: CIColorKernel = {
        let kernelString =
        """
        kernel vec4 colorize(__sample pixel, vec4 color) {
            pixel.rgb = pixel.a * color.rgb;
            pixel.a *= color.a;
            return pixel;
        }
        """
        return CIColorKernel(source: kernelString)!
    }()

    override var outputImage: CIImage? {
        guard let inputImage = inputImage, let inputColor = inputColor else { return nil }
        let inputs = [inputImage, inputColor] as [Any]
        return kernel.apply(extent: inputImage.extent, arguments: inputs)
    }
}

extension UIImage {
    func colorized(with color: UIColor) -> UIImage {
        guard let cgInput = self.cgImage else {
            return self
        }
        let colorFilter = ColorFilter()
        colorFilter.inputImage = CIImage(cgImage: cgInput)
        colorFilter.inputColor = CIColor(color: color)

        if let ciOutputImage = colorFilter.outputImage {
            let context = CIContext(options: nil)
            let cgImg = context.createCGImage(ciOutputImage, from: ciOutputImage.extent)
            return UIImage(cgImage: cgImg!, scale: self.scale, orientation: self.imageOrientation).alpha(color.rgba.alpha).withRenderingMode(self.renderingMode)
        } else {
            return self
        }
    }

At this point you should have everything to make this work:

extension UIImage {
    func stroked(with color: UIColor, size: CGFloat) -> UIImage {
        let strokeImage = self.colorized(with: color)
        let oldRect = CGRect(x: size, y: size, width: self.size.width, height: self.size.height).integral
        let newSize = CGSize(width: self.size.width + (2*size), height: self.size.height + (2*size))
        let translationVector = CGPoint(x: size, y: 0)

        UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
        if let context = UIGraphicsGetCurrentContext() {
            context.interpolationQuality = .high

            let step = 10 // reduce the step to increase quality
            for angle in stride(from: 0, to: 360, by: step) {
                let vector = translationVector.rotated(around: .zero, byDegrees: CGFloat(angle))
                let transform = CGAffineTransform(translationX: vector.x, y: vector.y)
                context.concatenate(transform)
                context.draw(strokeImage.cgImage!, in: oldRect)
                let resetTransform = CGAffineTransform(translationX: -vector.x, y: -vector.y)
                context.concatenate(resetTransform)
            }
            context.draw(self.cgImage!, in: oldRect)

            let newImage = UIImage(cgImage: context.makeImage()!, scale: self.scale, orientation: self.imageOrientation)
            UIGraphicsEndImageContext()

            return newImage.withRenderingMode(self.renderingMode)
        }
        UIGraphicsEndImageContext()
        return self
    }
}
herme5
  • 413
  • 5
  • 15
  • If we manually remove background the edges are not smooth. can you please suggest any technique about smoothing the border? – Ahmed Jun 14 '21 at 00:48
  • @Ahmed, short answer: Photo editing software like Gimp or Photoshop. I don't think the perfect algorithm exists for all photos (especially realistic ones, with landscapes). – herme5 Jul 26 '21 at 23:36
  • @Ahmed Nevertheless I have this method for images that are relatively simple (e.g: monochromatic logos typos cartoon-drawing with uniform background) Gimp has a filter that "erase" a color channel (e.g: every white pixels will become fully transparent). Take a look at this question: https://stackoverflow.com/questions/50216613/colorize-a-uiimage-in-swift you could rewrite a shader function so that: the more the pixel RGB is close to the color you want to erase, the less is the alpha. – herme5 Jul 26 '21 at 23:39
  • @Ahmed "The more the pixel RGB is close to the color you want to erase, the more the pixel is transparent". I don't know yet how you can model its mathematical representation in an algorithm. – herme5 Jul 27 '21 at 00:06
  • @herme5 so if i understand this correctly all your really doing is taking the source image and expanding the "border image" some amount of thickness larger than the original image? – Esko918 Apr 19 '22 at 20:45
  • 1
    @Esko918 that’s it – herme5 Apr 19 '22 at 21:07
  • @herme5 thank you dude – Esko918 Apr 20 '22 at 19:39
0

Borders to the images belongs to image processing area of iOS. It's not easy as borders for a UIView, It's pretty deep but if you're willing to go the distance, here is a library and a hint for the journey https://github.com/BradLarson/GPUImage

try using GPUImageThresholdEdgeDetectionFilter

or try OpenCV https://docs.opencv.org/2.4/doc/tutorials/ios/image_manipulation/image_manipulation.html

ZameerMoh
  • 1,149
  • 1
  • 17
  • 26
-4

Use this simple extension for UIImage

extension UIImage {

    func outline() -> UIImage? {

        UIGraphicsBeginImageContext(size)
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        self.draw(in: rect, blendMode: .normal, alpha: 1.0)
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(red: 1.0, green: 0.5, blue: 1.0, alpha: 1.0)
        context?.setLineWidth(5.0)
        context?.stroke(rect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage

    }

}

It will give you an image with pink border.

Arun Kumar
  • 788
  • 5
  • 15
  • I tried your code with: let image = UIImage(named: "plum")!.outline() and still get the border arounf the imge view and not around the image itself! – Roman Dec 20 '17 at 07:16
  • Its around the image not the image view. Do one thing , Make your imageview bigger and keep its content mode property set to aspect fit and give it a background color. You will se the border around the image, not around the imageView. – Arun Kumar Dec 20 '17 at 07:29
  • Hi Arun, I am trying to follow your suggestion but the result is not the right border. Could you give a short code for func perhaps, not only the extention? – Roman Dec 20 '17 at 07:59