33

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.

caleb
  • 391
  • 1
  • 3
  • 11

5 Answers5

32

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
}
App Dev Guy
  • 5,396
  • 4
  • 31
  • 54
Jorge Ramírez
  • 657
  • 1
  • 7
  • 8
19

It's simple:

  1. 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.)

  2. Draw (i.e. copy) the image into the context. (UIImage actually has draw... methods for this very purpose.)

  3. Draw your line into the context. (There are CGContext functions for this.)

  4. Extract the resulting image from the context. (For example, if you used UIGraphicsBeginImageContextWithOptions, you would use UIGraphicsGetImageFromCurrentImageContext.) Then close the context.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    I've written a book that you can read online that teaches you how to do this: http://www.apeth.com/iOSBook/ch15.html#_graphics_contexts – matt Oct 10 '16 at 01:41
  • In Swift 2, it's really all the same, because it's just a bunch of C function calls and Swift just calls them directly. Swift 3 is very different. – matt Oct 10 '16 at 01:49
  • Actually, using UIGraphicsBeginImageContextWithOptions and UIGraphicsGetImageFromCurrentImageContext will cost much more memory rather than using UIGraphicsImageRenderer API. – Chris Forever Apr 24 '20 at 03:01
  • 1
    @ChrisForever I always use image renderer nowadays because it's a better API, but I have no reason to believe there's a memory difference; an image context is an image context, a buffer the size of the image. – matt Apr 24 '20 at 03:14
12

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:

enter image description here

Child Image:

enter image description here


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

enter image description here


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

enter image description here


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

enter image description here

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)
        }
    }
}
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
10

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)
}
appsunited
  • 1,667
  • 16
  • 22
  • Bravo for this answer, that doesn't require a pre-existing image. This would be useful, for example in a function that converts CTFont()) character into an image (with `ctx.showGlyphs()`) – clearlight Apr 25 '22 at 16:35
6

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 }
Nadeesha Lakmal
  • 433
  • 5
  • 9
  • 2
    Elaborating on how this code answers the question will help future visitors. – JAL Jan 25 '17 at 18:24
  • sir, could you please help me on this one ? https://stackoverflow.com/questions/49919853/how-to-draw-doodle-line-on-uiimage-in-swift – sarah Apr 19 '18 at 11:24