I wrote extension to UIImage to create a border around an image that reflects image shape. You can specify width, color and alpha for the original image.
extension UIImage {
func withOutline(width: CGFloat, color: UIColor, alpha: CGFloat = 1.0) -> UIImage? {
guard let image = addTransparentPadding(width / 2), let ciImage = CIImage(image: image) else { return nil }
let context = CIContext(options: nil)
let expandedExtent = ciImage.extent
let expandedImage = ciImage
guard let alphaMaskFilter = CIFilter(name: "CIColorMatrix") else { return nil }
alphaMaskFilter.setValue(expandedImage, forKey: kCIInputImageKey)
alphaMaskFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputRVector")
alphaMaskFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputGVector")
alphaMaskFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputBVector")
alphaMaskFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
guard let alphaImage = alphaMaskFilter.outputImage else { return nil }
guard let edgeFilter = CIFilter(name: "CIMorphologyGradient") else { return nil }
edgeFilter.setValue(alphaImage, forKey: kCIInputImageKey)
edgeFilter.setValue(width, forKey: "inputRadius")
guard let edgeMaskImage = edgeFilter.outputImage else { return nil }
guard let constantColorFilter = CIFilter(name: "CIConstantColorGenerator") else { return nil }
constantColorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey)
guard let colorImage = constantColorFilter.outputImage else { return nil }
let coloredEdgeImage = colorImage.cropped(to: expandedExtent)
guard let colorClampFilter = CIFilter(name: "CIColorClamp") else { return nil }
colorClampFilter.setValue(edgeMaskImage, forKey: kCIInputImageKey)
colorClampFilter.setValue(CIVector(x: 1, y: 1, z: 1, w: 0.0), forKey: "inputMinComponents")
colorClampFilter.setValue(CIVector(x: 1.0, y: 1.0, z: 1.0, w: 1.0), forKey: "inputMaxComponents")
guard let colorClampImage = colorClampFilter.outputImage else { return nil }
guard let sharpenFilter = CIFilter(name: "CISharpenLuminance") else { return nil }
sharpenFilter.setValue(colorClampImage, forKey: kCIInputImageKey)
sharpenFilter.setValue(10.0, forKey: "inputSharpness") // Adjust sharpness level
sharpenFilter.setValue(10.0, forKey: "inputRadius") // Adjust radius
guard let shaprenImage = sharpenFilter.outputImage else { return nil }
colorClampFilter.setValue(CIVector(x: 0.0, y: 0.0, z: 0.0, w: 0.0), forKey: "inputMinComponents")
colorClampFilter.setValue(CIVector(x: 0.0, y: 0.0, z: 0.0, w: 1.0), forKey: "inputMaxComponents")
colorClampFilter.setValue(expandedImage, forKey: kCIInputImageKey)
guard let expandedMaskImage = colorClampFilter.outputImage else { return nil }
guard let compositeFilter = CIFilter(name: "CISourceOverCompositing") else { return nil }
compositeFilter.setValue(shaprenImage, forKey: kCIInputBackgroundImageKey)
compositeFilter.setValue(expandedMaskImage, forKey: kCIInputImageKey)
guard let maskImage = compositeFilter.outputImage else { return nil }
guard let blendFilter = CIFilter(name: "CIBlendWithMask") else { return nil }
blendFilter.setValue(coloredEdgeImage, forKey: kCIInputImageKey)
blendFilter.setValue(maskImage, forKey: kCIInputMaskImageKey)
guard let outlineImage = blendFilter.outputImage else { return nil }
let rgba = [0.0, 0.0, 0.0, alpha]
guard let colorMatrix = CIFilter(name: "CIColorMatrix") else { return nil }
colorMatrix.setDefaults()
colorMatrix.setValue(expandedImage, forKey: kCIInputImageKey)
colorMatrix.setValue(CIVector(values: rgba.map { CGFloat($0) }, count: 4), forKey: "inputAVector")
guard let alphaImage = colorMatrix.outputImage else { return nil }
compositeFilter.setValue(alphaImage, forKey: kCIInputImageKey)
compositeFilter.setValue(outlineImage, forKey: kCIInputBackgroundImageKey)
guard let finalImage = compositeFilter.outputImage else { return nil }
guard let cgImage = context.createCGImage(finalImage, from: expandedExtent) else { return nil }
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
}
func addTransparentPadding(_ padding: CGFloat) -> UIImage? {
let newSize = CGSize(width: self.size.width + (2 * padding),
height: self.size.height + (2 * padding))
UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
// Ensure transparency by setting a clear background
context.clear(CGRect(origin: .zero, size: newSize))
// Corrected origin positioning
let origin = CGPoint(x: padding, y: padding)
self.draw(in: CGRect(origin: origin, size: self.size))
let paddedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return paddedImage?.withRenderingMode(.alwaysOriginal)
}
}
Usage is simple
imageView.image = myImage.withOutline(width: 5, color: .red, alpha: 1)