Swift Swift: Making Background Images, Small Images and Custom UIViews with Autolayout

For those who have worked a bit with auto AutoRotationlayout, there are two topics that seems to be a real challenge: using a custom UIView, especially when using Core Graphics , and displaying photos  either in the foreground or as a background. Both suffer from similar problems. When changing device or changing orientations they are subject to distortion. In this lesson, we’ll discuss how to work with  auto layout and code to solve this by using aspect ratios. Auto Rotation changed in iOS8 We’ll also look at a few methods that will come in handy when working with Autorotation in iOS 8 and beyond.

Make a New Project

Start by making a new project called SwiftAspectRatio using Swift as the language and setting the device to Universal. When loaded, make sure all orientations are checked on.  Usually you will have to check on Upside Down:

2015-05-01_06-39-45

We’ll need some photos for this. Download the following two photos skyline.jpg and skyline@2x.jpg.  or click here SwiftAspectRatio.zip to download a .zip of them. Be sure to rename the Retina version  skyline@2x.jpg, since the download may change it.
Skyline           Skyline@2x

Click the images.xcassets folder in xcode. From where you downloaded the two photos in Finder, drag the photos to the assets.

2015-05-01_06-58-09

Giving the LaunchScreen a background

One of the cool and underused features of Xcode 6 is the lauchscreen.xib which lets you lay out a launch screen with auto layout. We’ll use this to explore backgrounds and spiff up the launch screen. Click on the launchScreen.Xib.

Click on the Large Label SwiftAspectRatioLayout. In the resolver, clear the constraints for this label. Using the pin menu, set the label 20 points up and 20 points left.  Select fr Update frames to Items of New Constraints. Add the two constraints.

2015-05-03_07-05-43

Change the object library to the media library.

2015-05-01_07-05-57

Drag the skyline picture into the storyboard. It will be too big for it. Drag it so you can see the upper left corner.

2015-05-01_07-15-19

Using the pin menu, pin the image 0 points top, 0 points left, 0 points right and 0 points down making sure that Constrain to Margins is unchecked. Update the Frames for Items of new Constraints like this:

2015-05-01_13-17-34

Our labels have disappeared since the image overlaps them. Move the image to the bottom of the display order. If the document outline is not open, click the storybaord outline icon Icon. In the document outline, drag  Skyline above the two labels.

2015-05-01_07-35-40

This places it behind the labels on the storyboard. Our photo is now a background.

2015-05-03_07-12-03

Open up the assistant editor. Click where it says either Automatic or Manual. Select Preview from the drop down menu. Click the plus on the bottom of the preview pane and add an iPad. Arrange your workspace like this by hiding the navigator to give your self a bit more room:

2015-05-03_07-14-28

You’ll notice the preview pane shows the skyline image on the iPad,  but it does not completely cover the view.

2015-05-03_07-16-50

Select the Skyline. In the attributes inspector, you will fiend the View attributes. Currently, the image gets centered. Change from Center to  Aspect Fill.

2015-05-02_10-40-41

The previews change.

 2015-05-03_07-19-41

Using the rotation Icon next to the device name rotate the iPad and phone. They look good.

With that much space, on the iPad,  the title is too high. Using size classes we can change that for the iPad. Click the size class selector and select regular width, regular height.

2015-05-03_07-21-00

Click on the title label. In the size inspector, change the constant to 200.

2015-05-03_07-24-32

The new iPad version looks better.

2015-05-03_07-26-24

Sizing a Photo Smaller than the Superview

Now that we made the picture resize to cover the full view, We can do the same thing with a picture smaller than the view. Go to the storyboard. Make the background of the view Black. Once again drag the skyline to the storyboard. It is still extremely big.

2015-05-03_07-29-52

We’ll pin the image to the lower right corner. Select the image and in the pin menu select 0 points right and 0 points bottom, this time leaving checked the Constrain to Margins. Do not update the frames yet. From the Image, control-drag to the black of the superview. In the menu that pops up, select Equal Heights.

Now go to the size inspector. Click Edit on the Equal Height to Superview. For the multiplier enter 1:2.

2015-05-01_19-52-00

You should get something that looks like this in the preview:

2015-05-03_07-35-59

If you get this instead,

2015-05-03_07-35-15

Change the multiplier to 2:1 to get what we want.

What we just did was pin the corner to the bottom corner of the view, then set the height of the picture to half the height of the view. This distorted or cropped the picture. As we did before, in the attribute inspector, select Aspect Fill. Not much changes. We need to set the aspect ratio on the view itself. With the image selected select Aspect ratio and also update Frame with new constraints in the pin menu.

2015-05-03_07-39-48

Again nothing happens. Check in the size inspector for the aspect ratio,

2015-05-03_07-42-59

The aspect ratio constrained both sides based on the current height in the storyboard. You need to specify your original aspect ratio when you cropped it in your photo editing software. I cropped this picture with a 2:3 ratio. Click Edit  for the aspect ratio. Change the aspect ratio to 2:3.

2015-05-03_07-45-30

While I did all of this with auto layout, It is very important to have a photo that works well. I used an image from my iPhone camera that I reduced by half in Photoshop for my retina image and another half for my non-retina image. I then cropped it to 2:3. Knowing your aspect ratio early in the process makes setting it in the app a lot easier. Since we know our numbers, we get this:

2015-05-03_07-46-32

Build and run with the iPhone 6 in the simulator. You can rotate the image by clicking Command Left-Arrow.

  2015-05-03_12-53-17 2015-05-03_12-53-33

Fixing Rotation and Orientation Problems

We have one rotation problem. When the phone is upside down, it does not auto rotate.

2015-05-03_12-53-47

This is the default rotation. To change the rotation so it rotates in all orientations, go to  ViewController.swift.  under the method  ViewDidload, add this:

override func supportedInterfaceOrientations() -> Int {
        return Int(UIInterfaceOrientationMask.All.rawValue)
    }

Orientations are found in a mask called UIInterfaceOrientationMask. The mask is an unsigned integer. This method returns an Int. To get an integer, we need to first get the raw value, and then cast it to Int. Build and run now, and we get rotation in all four orientations.

Adding a UIView.

Like the image, we can do the same with a UIView which has custom drawing in it. Go to the storyboard. Drag a view on to the storyboard. Set the background to light gray. Pin the view 0 points up and 0 left and select Aspect Ratio.

2015-05-03_13-15-39

In the size inspector,  Edit in the Aspect Ratio and make the multiplier 1: 1. For the skyline photo we controlled the size with the height. For the UIView, we’ll control with the width. Control-drag from the view to the black of the superview. Select Equal Widths. Go to the size inspector. Select Edit in the Equal Width to Superview. and make the multiplier 1:4 or 4:1,  whichever ne gives you a smaller gray box. Update all frames.  Build and run. The gray square works the same as the photo.

Let’s add some drawing code to the box. Stop the simulator and in Xcode, press Command-N. Add a UIView subclass named TestPatternView. Save the class. remove the comment markers from  the drawRect method. Add the following to drawRect:

//set up context
let context = UIGraphicsGetCurrentContext()
//set up color and line width
var fillcolor = UIColor.blueColor().CGColor
var strokecolor = UIColor.yellowColor().CGColor
let colorSpace = CGColorSpaceCreateDeviceRGB()
CGContextSetLineWidth(context, CGRectGetMaxX(bounds) * 0.02) //relative width
CGContextSetStrokeColorWithColor(context, strokecolor  )
CGContextSetFillColorWithColor(context, fillcolor)

This code does the set up for our drawing. We’ll start by drawing some circles.

let centerPoint:CGPoint = CGPoint(x: CGRectGetMidX(self.bounds), y: CGRectGetMidY(self.bounds))
let boundsRadius = bounds.size.width / 2.0
CGContextFillEllipseInRect(context, self.bounds)
CGContextAddArc(context, centerPoint.x, centerPoint.y, boundsRadius , 0.0, CGFloat(2.0 * M_PI), 1)
CGContextStrokePath(context)
CGContextAddArc(context, centerPoint.x, centerPoint.y, boundsRadius / 2.0, 0.0, CGFloat(2.0 * M_PI), 1)
CGContextStrokePath(context)
CGContextAddArc(context, centerPoint.x, centerPoint.y, boundsRadius / 4.0, 0.0, CGFloat(2.0 * M_PI), 1)
CGContextStrokePath(context)

Next, draw some lines from the corners and the midpoints of the sides.


//add lines
CGContextMoveToPoint(context, centerPoint.x, CGRectGetMinY(bounds))
CGContextAddLineToPoint(context, centerPoint.x, CGRectGetMaxY(bounds))

CGContextMoveToPoint(context, CGRectGetMinX(bounds),CGRectGetMinY(bounds))
CGContextAddLineToPoint(context, CGRectGetMaxX(bounds), CGRectGetMaxY(bounds))

CGContextMoveToPoint(context, CGRectGetMinX(bounds), centerPoint.y)
CGContextAddLineToPoint(context, CGRectGetMaxX(bounds), centerPoint.y)

CGContextMoveToPoint(context, CGRectGetMaxX(bounds), CGRectGetMinY(bounds))
CGContextAddLineToPoint(context, CGRectGetMinX(bounds), CGRectGetMaxY(bounds))
// draw the lines
CGContextStrokePath(context)

Add a small filled circle at the top middle of the drawing:


//draw a small circle at the top middle of the image
strokecolor = UIColor.redColor().CGColor
CGContextSetFillColorWithColor(context, strokecolor)
let radius = bounds.width * 0.1
var circleOrigin = CGPointMake(CGRectGetMidX(bounds) - radius, CGRectGetMinY(bounds))
let smallCircle = CGRectMake(circleOrigin.x,circleOrigin.y,radius * 2,radius * 2)
CGContextFillEllipseInRect(context, smallCircle)

Go back to the storyboard and select the gray square. In the identity inspector change the class to TestPatternView.

Set the Assistant editor to Automatic. Control-Drag from the gray square to the View Controller code. Add an outlet testPattern.

In the viewController class, add the following method:

override func viewDidLayoutSubviews() {
        testPattern.setNeedsDisplay()
    }

The method viewDidLayoutSubviews gets called after auto layout changes the view for this view controller. This happens on a rotation.  The view’s bounds and frame are set correctly by the time this method gets called. With auto layout, we cannot guarantee at viewDidLoad or viewWillAppear. Those two methods are not called when we rotate the device, viewDidLayoutSubviews  does. If you want to have the correct sizing and behavior on rotation, it is a good idea to place any code affected by rotation or auto layout here. With the one exception we’ve already talked about, iOS8 deprecated all rotation methods.

Build and run. Rotate and the view resizes.

AutoRotation

The key is to think relative, not absolute. Everything is based on proportions. The size of our photo is half the height. the size of our test pattern is a quarter of the width. I even do this in the drawRect method. As you rotate a device, you’ll notice that the line sizes stay proportional. That is from this line:

CGContextSetLineWidth(context, CGRectGetMaxX(bounds) * 0.02) //relative width

I made the line width 2% of the width of the view. No matter what the view does, it stays proportional. In the drawrect method, first change the 0.02 to 0.05, then add this just below the //add lines comment.

CGContextSetLineWidth(context, 2.0)

Run again on an ipad2 simulator and rotate:
2015-05-03_19-34-51   2015-05-03_19-35-04

The lines dont change. Change the 2.0 to CGRectGetMaxX(bounds) * 0.02 and we have a better line.
2015-05-03_19-36-40   2015-05-03_19-36-58

The Whole Code
Downloads of the code and resources can be found here: SwiftAspectRatio.zip

viewcontroller.swift

//
//  ViewController.swift
//  SwiftAspectRatio
//
//  Created by Steven Lipton on 5/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    @IBOutlet weak var testPattern: UIView!
    override func supportedInterfaceOrientations() -> Int {
        return Int(UIInterfaceOrientationMask.All.rawValue)
    }

    override func viewDidLayoutSubviews() {
        testPattern.setNeedsDisplay()
    }

}

testpatternview.swift

//
//  TestPatternView.swift
//  SwiftAspectRatio
//
//  Created by Steven Lipton on 5/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class TestPatternView: UIView {

    // Only override drawRect: if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func drawRect(rect: CGRect) {
        // Drawing code
        let context = UIGraphicsGetCurrentContext()
        //set up color and line width
        var fillcolor = UIColor.blueColor().CGColor
        var strokecolor = UIColor.yellowColor().CGColor
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        CGContextSetLineWidth(context, CGRectGetMaxX(bounds) * 0.05) //relative width
        CGContextSetStrokeColorWithColor(context, strokecolor  )
        CGContextSetFillColorWithColor(context, fillcolor)

        let centerPoint:CGPoint = CGPoint(x: CGRectGetMidX(self.bounds), y: CGRectGetMidY(self.bounds))
        let boundsRadius = bounds.size.width / 2.0
        CGContextFillEllipseInRect(context, self.bounds)
        CGContextAddArc(context, centerPoint.x, centerPoint.y, boundsRadius , 0.0, CGFloat(2.0 * M_PI), 1)
        CGContextStrokePath(context)
        CGContextAddArc(context, centerPoint.x, centerPoint.y, boundsRadius / 2.0, 0.0, CGFloat(2.0 * M_PI), 1)
        CGContextStrokePath(context)
        CGContextAddArc(context, centerPoint.x, centerPoint.y, boundsRadius / 4.0, 0.0, CGFloat(2.0 * M_PI), 1)
        CGContextStrokePath(context)

        //add lines
        CGContextSetLineWidth(context, CGRectGetMaxX(bounds) * 0.02)
        CGContextMoveToPoint(context, centerPoint.x, CGRectGetMinY(bounds))
        CGContextAddLineToPoint(context, centerPoint.x, CGRectGetMaxY(bounds))

        CGContextMoveToPoint(context, CGRectGetMinX(bounds),CGRectGetMinY(bounds))
        CGContextAddLineToPoint(context, CGRectGetMaxX(bounds), CGRectGetMaxY(bounds))

        CGContextMoveToPoint(context, CGRectGetMinX(bounds), centerPoint.y)
        CGContextAddLineToPoint(context, CGRectGetMaxX(bounds), centerPoint.y)

        CGContextMoveToPoint(context, CGRectGetMaxX(bounds), CGRectGetMinY(bounds))
        CGContextAddLineToPoint(context, CGRectGetMinX(bounds), CGRectGetMaxY(bounds))
        // draw the lines
        CGContextStrokePath(context)

        strokecolor = UIColor.redColor().CGColor
        CGContextSetFillColorWithColor(context, strokecolor)
        let radius = bounds.width * 0.1
        var circleOrigin = CGPointMake(CGRectGetMidX(bounds) - radius, CGRectGetMinY(bounds))
        let smallCircle = CGRectMake(circleOrigin.x,circleOrigin.y,radius * 2,radius * 2)
        CGContextFillEllipseInRect(context, smallCircle)
    }

}

2 Replies to “Swift Swift: Making Background Images, Small Images and Custom UIViews with Autolayout”

  1. Even allowing for the change to Swift 2.0, my app is only *partially* rotating. In Preview, only the toolbars are showing when I rotate to landscape. My code is as follows:

    override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return [.AllButUpsideDown]
    }

    Do you have any insights about what the problem could be?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s