Core Graphics on macOS Tutorial

Core Graphics is Apple’s 2D drawing engine for OS X. Discover how to build a great disc info app for OS X using Core Graphics to draw charts in this Core Graphics on OS X tutorial. By Ernesto García.

4.6 (5) · 1 Review

Save for later
Share

Update 9/22/16: This tutorial has been updated for Xcode 8 and Swift 3.

Update 9/22/16: This tutorial has been updated for Xcode 8 and Swift 3.

You’ve seen a lot of apps that depict beautiful graphics and stylish custom views. The best of them make a lasting impression, and you always remember them because they are just so pretty.

Core Graphics is Apple’s 2D drawing engine, and is one of the coolest frameworks in macOS and iOS. It has capacity to draw anything you can imagine, from simple shapes and text to more complex visual effects that include shadows and gradients.

In this Core Graphics on macOS tutorial, you’ll build up an app named DiskInfo to create a custom view that displays the available space and file distribution of a hard drive. It’s a solid example of how you can use Core Graphics to make a dull, text-based interface beautiful:

Disc information drawn with Core Graphics

Along the way you’ll discover how to:

  • Create and configure a custom view, the base layer for any graphical element
  • Implement live rendering so that you don’t have to build and run every time you change your graphics
  • Paint with code by working with paths, fills, clipping areas and strings
  • Use Cocoa Drawing, a tool available to AppKit apps, which defines higher level classes and functions

In the first part of this Core Graphics on macOS tutorial, you’ll implement the bar chart using Core Graphics, before moving on to learn how to draw the pie chart using Cocoa Drawing.

So put on your painter’s hat and get ready to learn how to color your world.

Getting Started

First, download the starter project for DiskInfo here. Build and run it now.

Original view without any Core Graphics drawing

The app lists all your hard drives, and when you click on one it shows detailed information.

Before going any further, have a look at the structure of the project to become familiar with the lay of the land:

sshot-starterproject-structure-xcode-edit

How about a guided tour?

  • ViewController.swift: The main view controller of the application
  • VolumeInfo.swift: Contains the implementation of the VolumeInfo class, which reads the information from the hard drive, and the FilesDistribution struct that handles the split between file types
  • NSColor+DiskInfo.swift and NSFont+DiskInfo.swift: Extensions that define constants with the default colors and fonts
  • CGFloat+Radians.swift: Extension that converts between degrees and radians via some helper functions
  • MountedVolumesDataSource.swift and MountedVolumesDelegate.swift: Implement the required methods to show disk information in the outline view

Note: The app shows the correct disk usage information, but for the sake this tutorial, it creates a random file distribution.

Calculating the real file distribution each time you run the app will quickly become a time drain and spoil all your fun, and nobody wants that.

Note: The app shows the correct disk usage information, but for the sake this tutorial, it creates a random file distribution.

Calculating the real file distribution each time you run the app will quickly become a time drain and spoil all your fun, and nobody wants that.

Creating a Custom View

Your first to-do is to create a custom view named GraphView. It’s where you’ll draw the pie and bar charts, so it’s pretty important.

You need to accomplish two objectives to create a custom view:

  1. Create an NSView subclass.
  2. Override draw(_:) and add some drawing code.

On a high level, it’s as easy as that. Follow the next steps to learn how to get there.

Make the NSView Subclass

Select the Views group in the Project Navigator. Choose File \ New \ File… and select the macOS \ Source \ Cocoa Class file template.

Click Next, and in the ensuing screen, name the new class GraphView. Make it a subclass of NSView, and make sure that the language is Swift.

Click Next and Create to save your new file.

Open Main.storyboard, and go the the View Controller Scene. Drag a Custom View from the Objects Inspector into the custom view as shown:

sshot-drag-customview

Select that new custom view, and in the Identity Inspector, set the class name to GraphView.

sshot-attrinspector-change-class

You need some constraints, so with the graph view selected, click on the Pin button in the Auto Layout toolbar. On the popup, set 0 for the Top, Bottom, Leading and Trailing constraints, and click the Add 4 Constraints button.

sshot-addconstraints-trim

Click the triangular Resolve Auto Layout Issues button in the Auto Layout toolbar, and under the Selected Views section, click on Update Frames — should it show as disabled, click anywhere to deselect the new GraphView, and then re-select it.

sshot-updateframes-2-trim

Override draw(_:)

Open GraphView.swift. You’ll see that Xcode created a default implementation of draw(_:). Replace the existing comment with the following, ensuring that you leave the call to the superclass method:

NSColor.white.setFill()
NSRectFill(bounds)

First you set the fill color to white, and then you call the NSRectFill method to fill the view background with that color.

Build and run.

sshot-build-run-whitecolor

Your custom view’s background has changed from standard gray to white.

first-custom-view

Yes, it’s that easy to create a custom drawn view.

Live Rendering: @IBDesignable and @IBInspectable

Xcode 6 introduced an amazing feature: live rendering. It allows you to see how your custom view looks in Interface Builder — without the need to build and run.

To enable it, you just need to add the @IBDesignable annotation to your class, and optionally, implement prepareForInterfaceBuilder() to provide some sample data.

Open GraphView.swift and add this just before the class declaration:

@IBDesignable

Now, you need to provide sample data. Add this inside the GraphView class:

  
var fileDistribution: FilesDistribution? {
  didSet {
    needsDisplay = true
  }
}

override func prepareForInterfaceBuilder() {
  let used = Int64(100000000000)
  let available = used / 3
  let filesBytes = used / 5
  let distribution: [FileType] = [
    .apps(bytes: filesBytes / 2, percent: 0.1),
    .photos(bytes: filesBytes, percent: 0.2),
    .movies(bytes: filesBytes * 2, percent: 0.15),
    .audio(bytes: filesBytes, percent: 0.18),
    .other(bytes: filesBytes, percent: 0.2)
  ]
  fileDistribution = FilesDistribution(capacity: used + available,
                                       available: available,
                                       distribution: distribution)
}

This defines the property fileDistribution that will store hard drive information. When the property changes, it sets the needsDisplay property of the view to true to force the view to redraw its content.

Then it implements prepareForInterfaceBuilder() to create a sample file distribution that Xcode will use to render the view.

Note: You can also change the visual attributes of your custom views in real time inside Interface Builder. You just need to add the @IBInspectable annotation to a property.

Note: You can also change the visual attributes of your custom views in real time inside Interface Builder. You just need to add the @IBInspectable annotation to a property.

Next up: make all the visual properties of the graph view inspectable. Add the following code inside the GraphView implementation:

  
// 1
fileprivate struct Constants {
  static let barHeight: CGFloat = 30.0
  static let barMinHeight: CGFloat = 20.0
  static let barMaxHeight: CGFloat = 40.0
  static let marginSize: CGFloat = 20.0
  static let pieChartWidthPercentage: CGFloat = 1.0 / 3.0
  static let pieChartBorderWidth: CGFloat = 1.0
  static let pieChartMinRadius: CGFloat = 30.0
  static let pieChartGradientAngle: CGFloat = 90.0
  static let barChartCornerRadius: CGFloat = 4.0
  static let barChartLegendSquareSize: CGFloat = 8.0
  static let legendTextMargin: CGFloat = 5.0
}

// 2
@IBInspectable var barHeight: CGFloat = Constants.barHeight {
  didSet {
    barHeight = max(min(barHeight, Constants.barMaxHeight), Constants.barMinHeight)
  }
}
@IBInspectable var pieChartUsedLineColor: NSColor = NSColor.pieChartUsedStrokeColor
@IBInspectable var pieChartAvailableLineColor: NSColor = NSColor.pieChartAvailableStrokeColor
@IBInspectable var pieChartAvailableFillColor: NSColor = NSColor.pieChartAvailableFillColor
@IBInspectable var pieChartGradientStartColor: NSColor = NSColor.pieChartGradientStartColor
@IBInspectable var pieChartGradientEndColor: NSColor = NSColor.pieChartGradientEndColor
@IBInspectable var barChartAvailableLineColor: NSColor = NSColor.availableStrokeColor
@IBInspectable var barChartAvailableFillColor: NSColor = NSColor.availableFillColor
@IBInspectable var barChartAppsLineColor: NSColor = NSColor.appsStrokeColor
@IBInspectable var barChartAppsFillColor: NSColor = NSColor.appsFillColor
@IBInspectable var barChartMoviesLineColor: NSColor = NSColor.moviesStrokeColor
@IBInspectable var barChartMoviesFillColor: NSColor = NSColor.moviesFillColor
@IBInspectable var barChartPhotosLineColor: NSColor = NSColor.photosStrokeColor
@IBInspectable var barChartPhotosFillColor: NSColor = NSColor.photosFillColor
@IBInspectable var barChartAudioLineColor: NSColor = NSColor.audioStrokeColor
@IBInspectable var barChartAudioFillColor: NSColor = NSColor.audioFillColor
@IBInspectable var barChartOthersLineColor: NSColor = NSColor.othersStrokeColor
@IBInspectable var barChartOthersFillColor: NSColor = NSColor.othersFillColor

// 3
func colorsForFileType(fileType: FileType) -> (strokeColor: NSColor, fillColor: NSColor) {
  switch fileType {
  case .audio(_, _):
    return (strokeColor: barChartAudioLineColor, fillColor: barChartAudioFillColor)
  case .movies(_, _):
    return (strokeColor: barChartMoviesLineColor, fillColor: barChartMoviesFillColor)
  case .photos(_, _):
    return (strokeColor: barChartPhotosLineColor, fillColor: barChartPhotosFillColor)
  case .apps(_, _):
    return (strokeColor: barChartAppsLineColor, fillColor: barChartAppsFillColor)
  case .other(_, _):
    return (strokeColor: barChartOthersLineColor, fillColor: barChartOthersFillColor)
  }
}

This is what the code above does:

  1. Declares a struct with constants — magic numbers in code are a no-no — you’ll use them throughout the tutorial.
  2. Declares all the configurable properties of the view as @IBInspectable and sets them using the values in NSColor+DiskInfo.swift. Pro tip: To make a property inspectable, you must declare its type, even when it’s obvious from the contents.
  3. Defines a helper method that returns the stroke and fill colors to use for a file type. It’ll come in handy when you draw the file distribution.

Open Main.storyboard and have a look at the graph view. It’s now white instead of the default view color, meaning that live rendering is working. Have patience if it’s not there right away; it may take a second or two to render.

sshot-render-white-trim

Select the graph view and open the Attributes Inspector. You’ll see all of the inspectable properties you’ve added.

sshot-ibdesignable-trim

From now on, you can choose to build and run the app to see the results, or just check it in Interface Builder.

Time to do some drawing.

Ernesto García

Contributors

Ernesto García

Author

Over 300 content creators. Join our team.