One of my readers, Andrew VanWagoner, decided to put together a short tutorial that follows up the iOS 8 Swift Tutorial I put together last month. So I’m sharing it here with you all! Andrew is a Senior Software Engineer working for Adobe in Lehi, Utah. You can find out more about Andrew at his site, http://thetalecrafter.com/ or follow him on Twitter at @thetalecrafter.
In Jameson Quave’s Swift tutorial he issued a challenge to use an image rather than Unicode character for a play and pause button on a table cell. Having most of my experience as a web engineer, my natural choice for an icon like this was SVG. After quite a bit of pain, and determination to not just use a raster image, I discovered another scalable alternative.
Trying to load an SVG image
I added a “play.svg” file to my project, replaced the UILabel with an UIImageView, and added the following to my controller.
playIcon.image = UIImage(named: "play")
Well, the first try didn’t work at all, even though Xcode will correctly identify the SVG file as an image and display a nice preview of it. When you run your app, all you get is a blank nothing.
So I took to Google to figure out what I had done wrong. To my surprise, UIImage doesn’t know how to deal with SVG natively. There are multiple projects that try to add support, but I’m still new to iOS in general, so building and including a 3rd party library took me a long time, and I couldn’t quite get it to work. And in my specific use case, most of the SVG features these libraries implement would go unused.
Along the way I found out that you can do drawing primitives using Core Graphics. Since these drawing functions are conceptually the same as SVG path instructions, and even more similar to 2d canvas drawing, this sounded much easier to me. It is also what the 3rd party SVG libraries do under the hood.
Creating a custom view
If you are following along with Jameson’s tutorial, replace the playIcon UILabel with a View in the storyboard, and in the Identity Inspector, give the view a Class name “PlayPauseIconView”. Then create a new Swift class file “PlayPauseIconView.swift”. We’ll need to import UIKit, and the class needs to be a subclass of UIView. We will want to use this same view to draw both the play and pause icon, so let’s add a member that keeps track of which one we should draw.
import UIKit
class PlayPauseIconView: UIView {
var isPlaying = false
}
In order to do custom drawing, we override the drawRect function. Just to keep things clean, put the code for drawing the play and pause icons into their own functions. Stub them out for now.
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
if isPlaying {
// already playing, so draw a pause icon
drawPausePathTo(context, boundedBy: rect)
} else {
// not playing, draw a play icon
drawPlayPathTo(context, boundedBy: rect)
}
}
func drawPlayPathTo(context: CGContextRef, boundedBy rect: CGRect) {
}
func drawPausePathTo(context: CGContextRef, boundedBy rect: CGRect) {
}
You’ll notice that in order to draw, we need a context to draw inside of. We can get the current context with UIGraphicsGetCurrentContext(). Then we decide which icon to draw and call the appropriate function.
For the play icon we want to draw a triangle in the middle of our view, taking roughly half of the visual space. To get a list of all of the drawing functions available to use, you can look at the CGContext Reference.
First we will set the color we want to draw, then guide the turtlecursor across the path we want, then fill in the shape. Using the provided rectangle as our point of reference, we can size our triangle to scale with the resolution of the device.
func drawPlayPathTo(context: CGContextRef, boundedBy rect: CGRect) {
CGContextSetFillColorWithColor(context, UIColor.blackColor().CGColor)
CGContextMoveToPoint(context, rect.width / 4, rect.height / 4)
CGContextAddLineToPoint(context, rect.width * 3 / 4, rect.height / 2)
CGContextAddLineToPoint(context, rect.width / 4, rect.height * 3 / 4)
CGContextAddLineToPoint(context, rect.width / 4, rect.height / 4)
CGContextFillPath(context)
}
Again, if you are familiar with other 2d drawing systems, including SVG path notation, this is pretty straightforward. Using the same Core Graphics functions, fill out your pause icon function.
func drawPausePathTo(context: CGContextRef, boundedBy rect: CGRect) {
CGContextSetFillColorWithColor(context, UIColor.blackColor().CGColor)
CGContextMoveToPoint(context, rect.width / 4, rect.height / 4)
CGContextAddLineToPoint(context, rect.width / 4, rect.height * 3 / 4)
CGContextAddLineToPoint(context, rect.width * 2 / 5, rect.height * 3 / 4)
CGContextAddLineToPoint(context, rect.width * 2 / 5, rect.height / 4)
CGContextAddLineToPoint(context, rect.width / 4, rect.height / 4)
CGContextFillPath(context)
CGContextMoveToPoint(context, rect.width * 3 / 4, rect.height / 4)
CGContextAddLineToPoint(context, rect.width * 3 / 4, rect.height * 3 / 4)
CGContextAddLineToPoint(context, rect.width * 3 / 5, rect.height * 3 / 4)
CGContextAddLineToPoint(context, rect.width * 3 / 5, rect.height / 4)
CGContextAddLineToPoint(context, rect.width * 3 / 4, rect.height / 4)
CGContextFillPath(context)
}
Now that we have our custom drawing code, update cell.playIcon.text calls to instead set isPlaying on your PlayPauseIconView. Go ahead and run your app, and check out your custom drawn icon.
You’ll probably want to fine tune the size of your view in the storyboard. I ended up with a 22×22 square.
You may also find that your icon isn’t updating properly when you start previewing a song. Core Graphics tries to draw your view once, and just keep reusing the already drawn layer, so it needs some way to know that your view has changed and needs to be redrawn. Setting isPlaying isn’t enough. Luckily UIView has a function that does exactly that.
setNeedsDisplay()
To make sure we always call this function when we change the isPlaying state, either add a didSet listener to isPlaying, or just add functions that set isPlaying and then call setNeedsDisplay. Make sure to update your controller to use the functions instead of directly setting isPlaying.
That’s it. You should now have a custom view that draws your play and pause icons, and it properly updates when you start and stop previewing a song.
If you are more ambitious, you can add a spinner for while the song is loading from the network, and put your pause icon in the middle. You can see how I did it in my project on github.
Update: The repository for this section has been updated to reflect the changes to Swift in Xcode Beta 3/4. The beta also adds access controls, which you can learn about here.
Thanks for the great tutorials. For anyone having an issue with setting isPlaying…I had to first cast to a as PlayPauseIconView otherwise it would crash at runtime with UIView not implementing isPlaying…not sure what I’m doing wrong. playIcon is declared properly as a PlayPauseIconView type in the TrackCell class.