Learning iOS Swift: Simple Image Cropping App

Background

Hello.  I needed some place (before I start the topic) to say why I started to making an app for image cropping. Hence I am writing this annoying paragraph.

One year back I started to making apps for Android as that time I had windows PC and android phone and off course time to experiment. I saw one wallpaper in some wallpaper thread in popular android forum which I liked very much. It was simply a blurred image with a circle revealing the actual image. I wanted to make an app which can do exactly the same thing with few taps. So I published my first ever app in android (see fig).  If you have time you can look into this app.

 

I started making more apps for android.

Since I have little knowledge in programming I wanted to learn the sweet swift language. But I know I can’t make apps with writing hello world app. So I started to make an app which required a Image Cropping as an option with pre-defined ratio.  So I thought I should share – how I made an image cropping app with Swift 2.

Image Crop

I wont call this a tutorial, instead its a “code with me” type write up 🙂

In order to make image cropping app I needed these

  • picking image from device(iPhone/iPad)
  • Display the full image
  • Drag-zoom enabled rectangle to select a area.
  • get the same area from the image as well.
  • Showing some pre-defined output image ratio.
  • Showing the result
  • Saving/ re-selecting image

** I know there must be better options making such apps. But I wanted to make it in my way. 

With my effort so far I have made this (see images)

First Thing First – Making an single view application with Xcode:

First make a single view application with default options. Name as we wish. For simplicity we will add toolbar and bar button  item. We will add three only 1) for picking image from iDevice 2) Confirming crop and 3) for giving few options (see crop options in image above).

Screen Shot 2016-01-09 at 2.14.21 PM

Single View Application Project

Hence we get our VC similar to this image.

Screen Shot 2016-01-09 at 2.22.42 PM

Toolbar and Bar Button Item in main VC

I also embedded the VC into Navigation Controller. Also I made another view controller to show the cropped image and with “Crop Again” and “Save” Button.

Setting up the VCS:

Screen Shot 2016-01-08 at 12.07.36 AM

Working VC and Crop Preview VC

Xcode creates first view controller’s class View Controller. Swift which got two functions viewDidLoad() and  didReceiveMemoryWarning() .  I have not added Image View in the storyboard. I will create it on viewDidLoad() function.  

Showing Image In UIImageView:
var imgView: UIImageView!
override func viewDidLoad() {
    super.viewDidLoad()
    imgView = UIImageView(frame: self.view.frame)
    imgView.contentMode = .ScaleAspectFit
    imgView.clipsToBounds = true
    self.view.addSubview(imgView)
}

Now choosing image from iDevices. Need to use UIImagePickerController and hence we need to use UIImagePickerControllerDelegate and  UINavigationControllerDelegate.  Then we make a variable picker

let picker = UIImagePickerController() and in the viewDidLoad function we pass delegate as picker.delegate = self 

Now we need to bind a function with the “Pick Image” button (control drag to VC in storyboard) like this –

Screen Shot 2016-01-09 at 2.50.37 PM

Adding function from storyboard

 Well I also made one variable to store chosen image. I put lot of print() to get the coordinates (my first swift programming :D)

@IBAction func AddImage(sender: AnyObject) {
   picker.allowsEditing = false
   picker.sourceType = .PhotoLibrary
   presentViewController(picker, animated: true, completion: nil)
}
func imagePickerController(
   picker: UIImagePickerController,
   didFinishPickingMediaWithInfo info: [String : AnyObject])
{

   chosenImage = info[UIImagePickerControllerOriginalImage] as! UIImage
   imgView.image = chosenImage
   calculateRect()
   if !added {
     self.makeCropAreaVisible()
     added = true
   }
   print("now crop area on top \(cropview.frame) ")
   dismissViewControllerAnimated(true, completion: nil)
   print("image found with width \(chosenImage.size.width) and height \(chosenImage.size.height)")
   print("image view frame width \(imgView.frame.width) and height \(imgView.frame.height)")
}
func imagePickerControllerDidCancel(picker:UIImagePickerController
{
   dismissViewControllerAnimated(true, completion: nil)
}

calculateRect() and makeCropAreaVisible() actually taking care of the are below the cropping rectangle. We will see them later.  

 

Creating UIView for Crop Area:

I simply added a UIView on the root view and made the view drag-able and zoomable.  So I had to write a custom UIView like this –

init(origin: CGPoint, width: CGFloat,   height: CGFloat) {
   super.init(frame: CGRectMake(0.0, 0.0, width, height))
   _cropoptions = CROP_OPTIONS()
   _cropoptions.Center = origin
   let insetRect = CGRectInset(self.bounds,lineWidth,lineWidth)
   self.path = UIBezierPath(roundedRect: insetRect, cornerRadius: 0.0)
   self.center = origin
   self.fillColor = UIColor(red: 0.09, green: 0.56, blue: 0.8, alpha: 0.2)
   _cropoptions.Width = self.bounds.width
   _cropoptions.Height = self.bounds.height
   self.backgroundColor = UIColor.clearColor()
   addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "panning:"))
   addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: "pinching:"))
}    
func panning(panGR: UIPanGestureRecognizer) {
   self.superview!.bringSubviewToFront(self)
   var translation = panGR.translationInView(self)
   translation = CGPointApplyAffineTransform(translation, self.transform)
   self.center.x += translation.x
   self.center.y += translation.y
   panGR.setTranslation(CGPointZero, inView: self)
   _cropoptions.Center = self.center
}

func pinching(pinchGR: UIPinchGestureRecognizer) {
   self.superview!.bringSubviewToFront(self)
   let scale = pinchGR.scale  
   self.transform = CGAffineTransformScale(self.transform, scale, scale)
   _cropoptions.Height = self.frame.height
   _cropoptions.Width = self.frame.width
   pinchGR.scale = 1.0
}

Also made the overridden drawrect like this –

override func drawRect(rect: CGRect) {
   self.fillColor.setFill()
   self.path.fill()
   self.path.lineWidth = self.lineWidth
   UIColor.whiteColor().setStroke()
   self.path.stroke() 
}


When added to view this will make a zoomable and drag-able rectangle like this.
Screen Shot 2016-01-09 at 3.13.16 PM.png

Now I wanted to calculate the same area in the image – hence I needed the co-ordinate in the view as well as in the image.

Getting Data from Crop Area:

Well to get the rectangle’s coordinate I needed some global variable –  for simplicity I added a swift file like this –

struct xy {
   var x: CGFloat!
   var y: CGFloat!
   mutating func xy(_x: CGFloat , _y: CGFloat){
      self.x = _x
      self.y = _y
   }
}
enum CROP_TYPE{
   case square, rect3x2,  rect2x3,  rect3x4,  rect4x3,  rect2x5,  rect5x2
   static let divs = [square : 1, rect3x2 : 5, rect2x3 : 5,rect3x4 : 7, rect4x3 : 7, rect2x5 : 7,rect5x2 : 7]
   static let muls = [square : xy(x: 0.5, y: 0.5), rect3x2 : xy(x: 3/5, y: 2/5), rect2x3 : xy(x: 2/5, y: 2/5),rect3x4 : xy(x: 3/7, y: 4/7), rect4x3 : xy(x: 4/7, y: 3/7), rect2x5 : xy(x: 2/7, y: 5/7),rect5x2 : xy(x: 5/7, y: 2/7)]
   static let names = [square : " 1:1 ", rect3x2 : " 3:2 ", rect2x3 : " 2:3 ",rect3x4 : " 3:4 ", rect4x3 : " 4:3 ", rect2x5 : " 2:5 ",rect5x2 : " 5:2 "]
   func Div()-> Int{
      if let ret = CROP_TYPE.divs[self]{
         return ret
      }else{
         return -1
      }
   } 
   func Muls()-> xy{
      if let ret = CROP_TYPE.muls[self]{
          return ret
      }else{
          return xy(x: 0.5,y: 0.5)
      }
   }
   func Name()-> String{
      if let ret = CROP_TYPE.names[self]{
         return ret
      }else{
         return "n.a."
      }
   }
}
struct CROP_OPTIONS {
   var Height: CGFloat!
   var Width: CGFloat!
   var Center: CGPoint!
}
struct FRAME {
   var Height: CGFloat!
   var Width: CGFloat!
}
var  _cropoptions: CROP_OPTIONS!
var _frame: CROP_OPTIONS!
var croptype: CROP_TYPE!

Almost self explanatory. The enum CROP_TYPE is the crop options – i added two functions for names and multiplication factor of the rect. This will be used in creating cropping rectangle and use is like this –

func makeCropAreaVisible(){ //making selected crop area
   cropview.removeFromSuperview()
   cropview = nil
   let min: CGFloat = imgviewrect.size.height > imgviewrect.size.width ? imgviewrect.size.width:imgviewrect.size.height
   cropview = CustomCropView(origin: self.view.center, width: min*croptype.Muls().x, height: min*croptype.Muls().y)
   self.view.addSubview(cropview)
}

func calculateRect(){ // getting same value from the image
   imgviewrect = AVMakeRectWithAspectRatioInsideRect(chosenImage.size, imgView.bounds)
   print (" Image Frame height:\(imgviewrect.size.height) width:\(imgviewrect.size.width) and x,y =( \(imgviewrect.origin.x) ,\(imgviewrect.origin.y) )" )
}

Giving user option to select W:H ratio:

To make Alert dialog with various option mentioned we need this function (bind with the option item button )

@IBAction func CropOptions(sender: AnyObject) {
   print("Choose crop options \(croptype.Name()))")
   let myActionSheet = UIAlertController(title: "Crop Options[\(croptype.Name())]", message: "Select Crop Ratio (width: height) ", preferredStyle: UIAlertControllerStyle.ActionSheet)
   let op1 = UIAlertAction(title: CROP_TYPE.square.Name(), style: UIAlertActionStyle.Default) { (action) in
      croptype = CROP_TYPE.square
      self.makeCropAreaVisible()
   }  

   let op2 = UIAlertAction(title: CROP_TYPE.rect3x2.Name(), style: UIAlertActionStyle.Default) { (action) in
      croptype = CROP_TYPE.rect3x2
      self.makeCropAreaVisible()
   }
   
   let op3 = UIAlertAction(title: CROP_TYPE.rect2x3.Name(), style: UIAlertActionStyle.Default) { (action) in
      croptype = CROP_TYPE.rect2x3
      self.makeCropAreaVisible()
   }

   let op4 = UIAlertAction(title: CROP_TYPE.rect3x4.Name(), style: UIAlertActionStyle.Default) { (action) in
      croptype = CROP_TYPE.rect3x4
      self.makeCropAreaVisible()
   }

   let op5 = UIAlertAction(title: CROP_TYPE.rect4x3.Name(), style: UIAlertActionStyle.Default) { (action) in
      croptype = CROP_TYPE.rect4x3
      self.makeCropAreaVisible()
   }

   let op6 = UIAlertAction(title: CROP_TYPE.rect2x5.Name(), style: UIAlertActionStyle.Default) { (action) in
      croptype = CROP_TYPE.rect2x5
      self.makeCropAreaVisible()
   }

   let op7 = UIAlertAction(title: CROP_TYPE.rect5x2.Name(), style: UIAlertActionStyle.Default) { (action) in
      croptype = CROP_TYPE.rect5x2
      self.makeCropAreaVisible()
   }

   let op10 = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel) { (action) in
      print("Cancel action button tapped")
   }

   myActionSheet.addAction(op1)
   myActionSheet.addAction(op2)
   myActionSheet.addAction(op3)
   myActionSheet.addAction(op4)
   myActionSheet.addAction(op5)
   myActionSheet.addAction(op6)
   myActionSheet.addAction(op7)
   myActionSheet.addAction(op10)

   self.presentViewController(myActionSheet, animated: true, completion: nil)  
}

Pushing cropped image to New VC:

Almost all done. Now we need to get the cropped image and push it to new view controller. With the “Crop” button I used segue and named it as “showCroppedImg” . But we need to perform or task “getting cropped image” before showing the second VC. Here is the solution

override func prepareForSegue(segue: UIStoryboardSegue?, sender: AnyObject?) {
   if segue!.identifier == "showCroppedImg" {
      retriveCroppedImage()
    }
}

And getting cropped image is as simple as this –

func retriveCroppedImage(){
   let yratio: CGFloat = imgviewrect.size.height / chosenImage.size.height
   let xratio: CGFloat = imgviewrect.size.width / chosenImage.size.width
   var cliprect = CGRectMake(_cropoptions.Center.x - _cropoptions.Width/2, _cropoptions.Center.y - _cropoptions.Height/2, _cropoptions.Width, _cropoptions.Height)
   
   print("cliprect top  \(cliprect.size)")
   cliprect.size.height =  cliprect.size.height / xratio;
   cliprect.size.width =  cliprect.size.width / xratio;
   cliprect.origin.x = cliprect.origin.x / xratio + imgviewrect.origin.x  / xratio
   cliprect.origin.y = cliprect.origin.y / yratio - imgviewrect.origin.y  / xratio
   print("cliprect  On Image \(cliprect)")

   let imageRef =  CGImageCreateWithImageInRect(chosenImage.CGImage, cliprect )
   croppedImg  = UIImage(CGImage: imageRef!, scale:  UIScreen.mainScreen().scale, orientation: chosenImage.imageOrientation)
   print("Operation complete");
}

Saving Image:

Now saving the image is in the second view controller.

@IBAction func SaveToAlbum(sender: AnyObject) {
   UIImageWriteToSavedPhotosAlbum(croppedImg, self, "image:didFinishSavingWithError:contextInfo:", nil)
}

func image(image: UIImage, didFinishSavingWithError error: NSErrorPointer, contextInfo:UnsafePointer<Void>) {
   if error != nil {
      let alert=UIAlertController(title: "Opps!", message: "Your Cropped Image could not be saved", preferredStyle: UIAlertControllerStyle.Alert);
      showViewController(alert, sender: self);
   }
}

All done. I am now making the app where I wanted to use crop functionality. I needed almost two week (read weekend) to make this functionality.   

Here is more screen shots with 5:2 and 3:4 ratio cropping 🙂

Here is another approach to crop image I recently wrote . Have a look in  this  post – which is more easier

new!! Dynamic Tableview( with sourcecode) – Swift 3.0

Home  iOS Swift Image Crop  Swift- Learning Stack Layout Get fonts list in iOS Device  Swift Text Customisation Page View Controller with Current Index 

Posted in Custom UIView, iOS, iOS Swift, swift Image Crop, System Fonts, Tutorials
11 comments on “Learning iOS Swift: Simple Image Cropping App
  1. comencan says:

    Hi Can u add your project files?

    Like

  2. […] iOS Swift Image Crop  Swift- Learning Stack Layout Get fonts list in iOS Device  Swift Text Customisation Page View […]

    Like

  3. DJ says:

    Waht are you doing in CustomCropView() method ?

    Like

    • Deb S says:

      CustomCropView() method calls the custom UIView. This custom UIView has pan and zoom handler registered. Actually it makes a view – which defines the ares for crop. The code under the heading “Creating UIView for Crop Area: ” is the class for CustomCropView.

      Like

  4. […] I only put the code to select the rectangle area – saving that rectangle is the easiest job (right ?) […]

    Like

  5. Lovish Dogra says:

    What are the ways to customize the crop box, like if we want an option come along crop box to delete this crop box etc?

    Like

    • Deb S says:

      For customisation you can add gesture recogniser and do what ever you want like which has been done in the example for zoom-in and zoom-out. Additionally you can have UITapGestureRecognizer

      //in constructor
      addGestureRecognizer(UITapGestureRecognizer(target: self, action: “ViewTapped:”))
      }

      //actual implementation
      func ViewTapped(tapGR: UITapGestureRecognizer){
      // i used delegate to get notified from my main view
      if(delegate != nil){
      delegate?.Tapped(“propic”)
      }
      print(“Tapped !!! “)
      }

      // now the protocol
      protocol CustomViewTapped{
      func Tapped(sender:String)
      }

      To remove the crop box from the view we can simply call this method –
      .removeFromSuperview().

      Hope this helped 🙂

      Like

  6. Lovish Dogra says:

    Thanks for this tutorial 🙂

    Like

  7. Daniel says:

    Source code for this?

    Like

    • Deb S says:

      Most of the codes are inside the blog post itself. I did it in a R&D manner – so there is no project which will match the same. I will compile one project and share the code

      Like

Leave a comment

Browse Post
Blog Stats
  • 36,170 hits
  • Design a site like this with WordPress.com
    Get started