I need to be able to programmatically draw on an image, and save that image for later use. Say, draw a line on specific x and y coordinates on the image, save the image, and display it onto a simple view controller. How would I go about doing this in Swift? (Preferably Swift 2, I am still in development and haven't updated my mac to Sierra)
Update: Possibly something to do with converting a UIImage to a CGLayer, drawing on it, and then converting it back to a UIImage.
All you need to do is create and get an Image Context object and access all its powerful drawing methods. You can learn more about the CGContext object features here.
This function draws a line and a circle on an UIImage and returns the modified image:
Swift 4
func drawOnImage(_ image: UIImage) -> UIImage {
// Create a context of the starting image size and set it as the current one
UIGraphicsBeginImageContext(image.size)
// Draw the starting image in the current context as background
image.draw(at: CGPoint.zero)
// Get the current context
let context = UIGraphicsGetCurrentContext()!
// Draw a red line
context.setLineWidth(2.0)
context.setStrokeColor(UIColor.red.cgColor)
context.move(to: CGPoint(x: 100, y: 100))
context.addLine(to: CGPoint(x: 200, y: 200))
context.strokePath()
// Draw a transparent green Circle
context.setStrokeColor(UIColor.green.cgColor)
context.setAlpha(0.5)
context.setLineWidth(10.0)
context.addEllipse(in: CGRect(x: 100, y: 100, width: 100, height: 100))
context.drawPath(using: .stroke) // or .fillStroke if need filling
// Save the context as a new UIImage
let myImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Return modified image
return myImage
}
It's simple:
Make an image graphics context. (Before iOS 10, you would do this by calling UIGraphicsBeginImageContextWithOptions. In iOS 10 there's another way, UIGraphicsImageRenderer, but you don't have to use it if you don't want to.)
Draw (i.e. copy) the image into the context. (UIImage actually has draw... methods for this very purpose.)
Draw your line into the context. (There are CGContext functions for this.)
Extract the resulting image from the context. (For example, if you used UIGraphicsBeginImageContextWithOptions, you would use UIGraphicsGetImageFromCurrentImageContext.) Then close the context.
Details
Xcode 9.1, Swift 4
Solution
extension UIImage
extension UIImage {
typealias RectCalculationClosure = (_ parentSize: CGSize, _ newImageSize: CGSize)->(CGRect)
func with(image named: String, rectCalculation: RectCalculationClosure) -> UIImage {
return with(image: UIImage(named: named), rectCalculation: rectCalculation)
}
func with(image: UIImage?, rectCalculation: RectCalculationClosure) -> UIImage {
if let image = image {
UIGraphicsBeginImageContext(size)
draw(in: CGRect(origin: .zero, size: size))
image.draw(in: rectCalculation(size, image.size))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
return self
}
}
extension UIImageView
extension UIImageView {
enum ImageAddingMode {
case changeOriginalImage
case addSubview
}
func drawOnCurrentImage(anotherImage: UIImage?, mode: ImageAddingMode, rectCalculation: UIImage.RectCalculationClosure) {
guard let image = image else {
return
}
switch mode {
case .changeOriginalImage:
self.image = image.with(image: anotherImage, rectCalculation: rectCalculation)
case .addSubview:
let newImageView = UIImageView(frame: rectCalculation(frame.size, image.size))
newImageView.image = anotherImage
addSubview(newImageView)
}
}
}
Images samples
Parent Image:
Child Image:
Usage example 1
func sample1(imageView: UIImageView) {
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "parent")?.with(image: "child") { parentSize, newImageSize in
print("parentSize = \(parentSize)")
print("newImageSize = \(newImageSize)")
return CGRect(x: 50, y: 50, width: 90, height: 90)
}
}
Result 1
Usage example 2
func sample2(imageView: UIImageView) {
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "parent")
imageView.drawOnCurrentImage(anotherImage: UIImage(named: "child"), mode: .changeOriginalImage) { parentSize, newImageSize in
print("parentSize = \(parentSize)")
print("newImageSize = \(newImageSize)")
let sideLength:CGFloat = 90
let indent:CGFloat = 50
return CGRect(x: parentSize.width-sideLength-indent, y: parentSize.height-sideLength-indent, width: sideLength, height: sideLength)
}
}
Result 2
Usage example 3
func sample3(imageView: UIImageView) {
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.image = UIImage(named: "parent")
imageView.drawOnCurrentImage(anotherImage: UIImage(named: "child"), mode: .addSubview) { parentSize, newImageSize in
print("parentSize = \(parentSize)")
print("newImageSize = \(newImageSize)")
let sideLength:CGFloat = 90
let indent:CGFloat = 15
return CGRect(x: parentSize.width-sideLength-indent, y: indent, width: sideLength, height: sideLength)
}
}
Result 3
Full sample code
Don't forget to add Solution code here
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView(frame: UIScreen.main.bounds)
view.addSubview(imageView)
sample1(imageView: imageView)
// sample2(imageView: imageView)
// sample3(imageView: imageView)
}
func sample1(imageView: UIImageView) {
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "parent")?.with(image: "child") { parentSize, newImageSize in
print("parentSize = \(parentSize)")
print("newImageSize = \(newImageSize)")
return CGRect(x: 50, y: 50, width: 90, height: 90)
}
}
func sample2(imageView: UIImageView) {
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "parent")
imageView.drawOnCurrentImage(anotherImage: UIImage(named: "child"), mode: .changeOriginalImage) { parentSize, newImageSize in
print("parentSize = \(parentSize)")
print("newImageSize = \(newImageSize)")
let sideLength:CGFloat = 90
let indent:CGFloat = 50
return CGRect(x: parentSize.width-sideLength-indent, y: parentSize.height-sideLength-indent, width: sideLength, height: sideLength)
}
}
func sample3(imageView: UIImageView) {
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.image = UIImage(named: "parent")
imageView.drawOnCurrentImage(anotherImage: UIImage(named: "child"), mode: .addSubview) { parentSize, newImageSize in
print("parentSize = \(parentSize)")
print("newImageSize = \(newImageSize)")
let sideLength:CGFloat = 90
let indent:CGFloat = 15
return CGRect(x: parentSize.width-sideLength-indent, y: indent, width: sideLength, height: sideLength)
}
}
}
Since iOS 10 you can use the UIGraphicImageRenderer, which has a better syntax and has some great features!
Swift 4
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
let image = renderer.image { context in
// draw your image into your view
context.cgContext.draw(UIImage(named: "myImage")!.cgImage!, in: view.frame)
// draw even more...
context.cgContext.setFillColor(UIColor.red.cgColor)
context.cgContext.setStrokeColor(UIColor.black.cgColor)
context.cgContext.setLineWidth(10)
context.cgContext.addRect(view.frame)
context.cgContext.drawPath(using: .fillStroke)
}
Updated Answer: Once you get the From and To coordinates, here is how to draw a line in a UIImage with those coordinates. From and To coordinates are in image pixels.
func drawLineOnImage(size: CGSize, image: UIImage, from: CGPoint, to: CGPoint) -> UIImage {
// begin a graphics context of sufficient size
UIGraphicsBeginImageContext(size)
// draw original image into the context
image.drawAtPoint(CGPointZero)
// get the context for CoreGraphics
let context = UIGraphicsGetCurrentContext()
// set stroking width and color of the context
CGContextSetLineWidth(context, 1.0)
CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)
// set stroking from & to coordinates of the context
CGContextMoveToPoint(context, from.x, from.y)
CGContextAddLineToPoint(context, to.x, to.y)
// apply the stroke to the context
CGContextStrokePath(context)
// get the image from the graphics context
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
// end the graphics context
UIGraphicsEndImageContext()
return resultImage }
Related
I am trying to get a navigation bar similar to the one in iOS messages app. This is what I have, and it creates a bit of a circle shape but gets cut off. If you were to recreate the centered image from Messages how would you?
let imageView = UIImageView(image: UIImage(named: "test-image"))
imageView.contentMode = .scaleAspectFit
let titleView = UIView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
imageView.frame = titleView.bounds
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = imageView.frame.height / 2
titleView.addSubview(imageView)
self.navigationItem.titleView = titleView
The current outcome with this snippet:
The desired outcome:
Tested solutions In dark mode:
Please excuse my eggs ;) It's my favorite test pic
Set image view content mode .scaleAspectFill
imageView.contentMode = .scaleAspectFill
The following is what I have done.
I use two functions to work with an image. The makeCutout function creates a square cutout based on the height of an image of your selection. The position of the cutout is placed at the center of the source image. (See let cutoutRect = ...) The second one, the makeCircularImage function, makes the cutout image circular. Note that I'm assuming the original image is horizontally-long, which means that the diameter of the cutout will be the height of the image.
import UIKit
class ViewController: UIViewController {
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
if let image = UIImage(named: "jenniferGarner.jpg") {
let imageWidth = image.size.width
let imageHeight = image.size.height
let cutoutRect = CGRect(origin: CGPoint(x: (imageWidth - imageHeight) / 2.0, y: 0.0), size: CGSize(width: imageHeight, height: imageHeight))
if let cutoutImage = makeCutout(image: image, rect: cutoutRect) {
/* making it a circular image */
if let circleImage = makeCircularImage(sourceImage: cutoutImage, diameter: cutoutImage.size.height) {
testImageView.isHidden = true
let imageView = UIImageView(image: circleImage)
imageView.contentMode = .scaleAspectFit
let titleView = UIView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
//titleView.backgroundColor = UIColor.green
imageView.frame = titleView.bounds
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = imageView.frame.height / 2
titleView.addSubview(imageView)
self.navigationItem.titleView = titleView
}
}
}
}
// MARK: - Making a cutout image
func makeCutout(image: UIImage, rect: CGRect) -> UIImage? {
if let cgImage = image.cgImage {
let contextImage: UIImage = UIImage(cgImage: cgImage)
// Create bitmap image from context using the rect
var croppedContextImage: CGImage? = nil
if let contextImage = contextImage.cgImage {
if let croppedImage = contextImage.cropping(to: rect) {
croppedContextImage = croppedImage
}
}
// Create a new image based on the imageRef and rotate back to the original orientation
if let croppedImage: CGImage = croppedContextImage {
let image: UIImage = UIImage(cgImage: croppedImage, scale: image.scale, orientation: image.imageOrientation)
return image
}
}
return nil
}
func makeCircularImage(sourceImage: UIImage, diameter: CGFloat) -> UIImage? {
let imageView = UIImageView(image: sourceImage)
let layer = imageView.layer
layer.masksToBounds = true
layer.cornerRadius = diameter / 2.0
UIGraphicsBeginImageContext(imageView.bounds.size)
layer.render(in: UIGraphicsGetCurrentContext()!)
if let finalImage = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
return finalImage
}
return nil
}
}
I have Jeniffer Garner as a guest as shown below. The source image size is 480 px X 180 px.
The following is the final work with an iPhone simulator with the dark mode.
I'm trying to create a UIImage using a provided UIBezierPath. Unfortunately, no matter what I set setLineWidth to, the result is always a stroke of 1 point:
extension UIBezierPath {
func image(fillColor: UIColor, strokeColor: UIColor) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 1.0)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
context.setLineWidth(10)
context.setFillColor(fillColor.cgColor)
context.setStrokeColor(strokeColor.cgColor)
self.fill()
self.stroke()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Trying this out in a test project with a circle, for example:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView()
imageView.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
view.addSubview(imageView)
let bezierPath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
let image = bezierPath.image(fillColor: UIColor.blue, strokeColor: UIColor.red)
imageView.image = image
}
}
No matter what I set setLineWidth to, it always appears to be 1 point.
You are calling stroke on the UIBezierPath, so you need to set the lineWidth property of that using self.lineWidth = 10.
extension UIBezierPath {
func image(fillColor: UIColor, strokeColor: UIColor) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 1.0)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
context.setFillColor(fillColor.cgColor)
self.lineWidth = 10
context.setStrokeColor(strokeColor.cgColor)
self.fill()
self.stroke()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
I want to screenshot a view and create an UIImage from it. I want the transparency attribute of the view to be maintained in my image. I tried this method after creating an extension of UIImage but the background is not transparent in the resultant image when uploaded to the server.
Kindly help or point me if I am doing something wrong!! This means that the resultant png was not having transparency.
class func createTransparentImageFrom(label: UILabel, imageSize: CGSize) -> UIImage {
UIGraphicsBeginImageContextWithOptions(imageSize, false, 2.0)
let currentView = UIView.init(frame: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
currentView.backgroundColor = UIColor.clear
currentView.addSubview(label)
currentView.layer.render(in: UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
This was due to the fact that when we render a view the image is JPEG/JPG, so we need to convert it to png in order to get layer information.
Try this :
extension UIImage {
class func createTransparentPNGFrom(label: UILabel, imageSize: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
let currentView = UIView.init(frame: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
currentView.backgroundColor = UIColor.clear
currentView.addSubview(label)
currentView.layer.render(in: UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let newImg = img else {
return nil
}
guard let imageData = UIImagePNGRepresentation(newImg) else {
return nil
}
return UIImage.init(data: imageData)
}
}
Swift 5 UIImage extension: screen UIView with transparent background
this is for taking a screenshot of a view which should have .backgroundColor = .clear
extension UIView {
func takeSnapshotOfView() -> UIImage? {
let size = CGSize(width: frame.size.width, height: frame.size.height)
let rect = CGRect.init(origin: .init(x: 0, y: 0), size: frame.size)
UIGraphicsBeginImageContext(size)
drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let imageData = image?.pngData() else {
return nil
}
return UIImage.init(data: imageData)
}
}
You need to put opaque value true for transparent result
Extension of UIView Of transparent screenshot
func screenShot() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, UIScreen.main.scale)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
I have a Drawing View which is on a Scroll View. After drawing is completed I need a screenshot of the drawing which I will be uploading to the server.
I am using UIBezeir path to draw on the view.
let path = UIBezierPath()
for i in self.drawView.path{
path.append(i)
}
self.drawView.path is an NSArray with all the bezeir paths of the drawing.
But when I use the bounding box of this path and get max and min values of coordinates and try to capture a screenshot I get this
var rect:CGRect = CGRect(x: path.bounds.minX, y: path.bounds.minY, width: path.bounds.maxX, height: path.bounds.maxY)
I also tried to give the bounds of the path itself
let rect:CGRect = CGRect(x: path.bounds.origin.x - 5, y: path.bounds.origin.y - 5, width: path.bounds.size.width + 5, height: path.bounds.size.height + 5)
Just for reference I tried using this rect and create a view (clear color with border layer) and placed it over the Drawing, it work pretty fine but when I try to capture an image it goes out of bounds
This is the function I am using to capture the screen
func imgScreenShot(bounds:CGRect) -> UIImage{
let rect: CGRect = bounds
self.drawView.isOpaque = false
self.drawView.backgroundColor = UIColor.clear
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
var context: CGContext? = UIGraphicsGetCurrentContext()
if let aContext = context {
self.drawView.layer.render(in: aContext)
}
var capturedImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//let finalImage = scaleImage(image: capturedImage)
return capturedImage!
}
I am also tried getting a UIView with this function
let vw = self.drawView.resizableSnapshotView(from: rect, afterScreenUpdates: true, withCapInsets: UIEdgeInsets.zero)
This gives me a perfect UIView with the drawing in that, but again when I try to convert the UIView to UIImage using the function giving the views bounds, I get a blank image.
Can anyone suggest what I am doing wrong or any other solution for how I can get this, bounds of image starting right exactly at the bounds of the drawing
let vw = self.drawView.resizableSnapshotView(from: rect2, afterScreenUpdates: true, withCapInsets: UIEdgeInsets.zero)
vw?.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
vw?.layer.borderColor = UIColor.red.cgColor
vw?.layer.borderWidth = 1
self.drawView.addSubview(vw!)
let image = vw?.snapshotImage
let imgView = UIImageView(frame: CGRect(x: 250, y: 50, width: 100, height: 100))
imgView.layer.borderColor = UIColor.gray.cgColor
imgView.layer.borderWidth = 1
self.drawView.addSubview(imgView)
Make an extension of UIView and UIImage , so in whole application lifecycle you can use those methods(which one i will be describe at below) for capture the screenshort of any perticular UIView and resize the existing image(if needed).
Here is the extension of UIView :-
extension UIView {
var snapshotImage : UIImage? {
var snapShotImage:UIImage?
UIGraphicsBeginImageContext(self.frame.size)
if let context = UIGraphicsGetCurrentContext() {
self.layer.render(in: context)
if let image = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
snapShotImage = image
}
}
return snapShotImage
}
}
Here is the extension of UIImage :-
extension UIImage {
func resizeImage(newSize:CGSize) -> UIImage? {
var newImage:UIImage?
let horizontalRatio = newSize.width / size.width
let verticalRatio = newSize.height / size.height
let ratio = max(horizontalRatio, verticalRatio)
let newSize = CGSize(width: size.width * ratio, height: size.height * ratio)
UIGraphicsBeginImageContext(newSize)
if let _ = UIGraphicsGetCurrentContext() {
draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: newSize))
if let image = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
newImage = image
}
}
return newImage
}
}
How to use those functions in our desired class ?
if let snapImage = yourUIView.snapshotImage {
///... snapImage is the desired image you want and its dataType is `UIImage`.
///... Now resize the snapImage into desired size by using this one
if let resizableImage = snapImage.resizeImage(newSize: CGSize(width: 150.0, height: 150.0)) {
print(resizableImage)
}
}
here yourUIView means , the one you have taken for drawing some inputs. it can be IBOutlet as well as your UIView (which you have taken programmatically)
The image becomes blurry once applying roundImage:
Making a UIImage to a circle form
extension UIImage
{
func roundImage() -> UIImage
{
let newImage = self.copy() as! UIImage
let cornerRadius = self.size.height/2
UIGraphicsBeginImageContextWithOptions(self.size, false, 1.0)
let bounds = CGRect(origin: CGPointZero, size: self.size)
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
newImage.drawInRect(bounds)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return finalImage
}
}
Why are you using bezierpath? Just set cornerradius for uiimageview.
If your image is larger than the imageview then you have to resize your image to your imageview size and then set cornerradius for that uiimageview.
It will work. Works for me
Replace the following line
UIGraphicsBeginImageContextWithOptions(self.size, false, 1.0)
with
UIGraphicsBeginImageContextWithOptions(self.size, view.opaque , 0.0)
try this one
let image = UIImageView(frame: CGRectMake(0, 0, 100, 100))
I recommend that you can use AlamofireImage (https://github.com/Alamofire/AlamofireImage)
It's very easily to make rounded image or circle image without losing quality.
just like this:
let image = UIImage(named: "unicorn")!
let radius: CGFloat = 20.0
let roundedImage = image.af_imageWithRoundedCornerRadius(radius)
let circularImage = image.af_imageRoundedIntoCircle()
Voila!
Your issue is that you are using scale 1, which is the lowest "quality".
Setting the scale to 0 will use the device scale, which just uses the image as is.
A side note: Functions inside a class that return a new instance of that class can be implemented as class functions. This makes it very clear what the function does. It does not manipulate the existing image. It returns a new one.
Since you were talking about circles, I also corrected your code so it will now make a circle of any image and crop it. You might want to center this.
extension UIImage {
class func roundImage(image : UIImage) -> UIImage? {
// copy
guard let newImage = image.copy() as? UIImage else {
return nil
}
// start context
UIGraphicsBeginImageContextWithOptions(newImage.size, false, 0.0)
// bounds
let cornerRadius = newImage.size.height / 2
let minDim = min(newImage.size.height, newImage.size.width)
let bounds = CGRect(origin: CGPointZero, size: CGSize(width: minDim, height: minDim))
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
// new image
newImage.drawInRect(bounds)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// crop
let maybeCrop = UIImage.crop(finalImage, cropRect: bounds)
return maybeCrop
}
class func crop(image: UIImage, cropRect : CGRect) -> UIImage? {
guard let imgRef = CGImageCreateWithImageInRect(image.CGImage, cropRect) else {
return nil
}
return UIImage(CGImage: imgRef)
}
}