Tag Archives: swift

CorePlot, NSDecimal and Swift

Recently, I shared a post that demonstrated how to integrate CorePlot into your Swift-based iOS project. That post covered the basics of how to get started, but there’s an issue with using CorePlot which can cause problems pretty quickly once you get into building your graphs.

CorePlot makes heavy use of the NSDecimal type in Objective-C, which is a special C struct used to store decimal numbers. Swift doesn’t directly support NSDecimal, so any methods that use it in CorePlot APIs are simply missing when you try to access them from Swift.

If you haven’t checked out the previous post, you’ll want to do that first. Starting from that app, we’ll replace all of the graph drawing code with some new code to build a scatter plot. We’ll just have some mock data, with four records, each with the x and y values set to the same number (1 through 4):


//
// ViewController.swift
// CorePlotTest
//
// Created by Al Wold on 8/6/14.
// Copyright (c) 2014 Al Wold. All rights reserved.
//
import UIKit
class ViewController: UIViewController, CPTPlotDataSource {
@IBOutlet weak var graphView: CPTGraphHostingView!
override func viewDidLoad() {
super.viewDidLoad()
// create graph
var graph = CPTXYGraph(frame: CGRectZero)
graph.title = "Hello Graph"
var plot = CPTScatterPlot()
plot.dataSource = self
graph.addPlot(plot)
self.graphView.hostedGraph = graph
}
func numberOfRecordsForPlot(plot: CPTPlot!) -> UInt {
return 4
}
func numberForPlot(plot: CPTPlot!, field fieldEnum: UInt, recordIndex idx: UInt) -> NSNumber! {
return idx+1
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

You should get something like this:

Screenshot 2014-08-29 18.04.01

As you can see, there’s nothing there. This is because the range on our plot isn’t big enough to show the graph. We need to set the length values for the x and y ranges on the default plot space. Our approach will be like this:

  • Get the default plot space for the graph
  • Get the x range of the plot space
  • Create a mutable copy of the x range
  • Set the x range’s length
  • Set the x range on the plot space to our new mutable copy
  • Repeat for the y range

Everything goes fine until you try to set the length of the x range. The length property on the CPTMutablePlotRange is represented as an NSDecimal, so it isn’t bridged into Swift.

Luckily, there’s a pretty easy workaround. By adding a Category for the classes you are using, you can add wrapper methods which use a supported Swift data type. In our case, we’ll be wrapping the CPTMutablePlotRange object in CorePlot, so that we can set the area displayed by our graph.

  • In XCode, select File->New->File
  • Select iOS->Source->Objective-C File
  • For the File Type, select Category, then fill it in like this:


Screenshot 2014-09-02 15.25.21

 

This will create a file called CPTMutablePlotRange+SwiftCompat.h/m. All of the methods we add in the category will be accessible each time we encounter a CPTMutablePlotRange in our app. We’ll just add a method to set the length as a float instead of an NSDecimal:


//
// CPTMutablePlotRange+SwiftCompat.h
// CorePlotTest
//
// Created by Al Wold on 8/29/14.
// Copyright (c) 2014 Al Wold. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "CPTMutablePlotRange.h"
@interface CPTMutablePlotRange (SwiftCompat)
– (void)setLengthFloat:(float)lengthFloat;
@end


//
// CPTMutablePlotRange+SwiftCompat.m
// CorePlotTest
//
// Created by Al Wold on 8/29/14.
// Copyright (c) 2014 Al Wold. All rights reserved.
//
#import "CPTMutablePlotRange+SwiftCompat.h"
@implementation CPTMutablePlotRange (SwiftCompat)
– (void)setLengthFloat:(float)lengthFloat
{
NSNumber *number = [NSNumber numberWithFloat:lengthFloat];
[self setLength:[number decimalValue]];
}
@end

The last step is to make sure to import CPTMutablePlotRange+SwiftCompat.h in your bridging header (see the previous post for how to set up your bridging header). Once you do that, you should be able to access the new setLengthFloat method any time you encounter a CPTMutablePlotRange.

So, let’s add some more code to the ViewController.swift to set the lengths:


var plotSpace = graph.defaultPlotSpace as CPTXYPlotSpace
var xRange = plotSpace.xRange.mutableCopy() as CPTMutablePlotRange
var yRange = plotSpace.yRange.mutableCopy() as CPTMutablePlotRange
xRange.setLengthFloat(10)
yRange.setLengthFloat(10)
plotSpace.xRange = xRange
plotSpace.yRange = yRange

view raw

gistfile1.swift

hosted with ❤ by GitHub

Once this is added, we run the app and get a graph that looks like this:

Screenshot 2014-08-29 18.07.58

Cool. It’s not very pretty, but it’s showing all of our data now. As you come across other methods/properties that need an NSDecimal, you can create/add to your categories to wrap the functions.

Eventually, I imagine this will be solved either via Swift adding a way to deal with NSDecimal types, or by CorePlot switching to another data type, but for now it’ll get you by. There’s some discussion on the CorePlot github, which you may want to keep an eye on:

https://github.com/core-plot/core-plot/issues/96

The full code for this is available in the sample app for the previous post, under the nsdecimal-workaround branch here:

https://github.com/alwold/CorePlotTest/tree/nsdecimal-workaround

Using CorePlot with Swift in iOS

Apple_Swift_LogoI just started working on a new iOS app right after the Swift announcement, and decided to try writing it in Swift. I needed to add some graphs to the app, and after looking at the options, I decided to give CorePlot a try. I wasn’t sure how things would work out with CocoaPods and a third party Objective-C library from a Swift app, but I was able to build something that works pretty well, so I’m going to outline how to do this with a quick sample app.

If you want to go straight to the code, my sample app is available on GitHub.

  1. The first step is to create your swift app. For our sample, we’re going to do a single view app, so create that in XCode, and set the default language to Swift
  2. Next, we’ll add CocoaPods as usual. Check out their site for more info how to install it, etc. But, once you have CocoaPods, go into your project dir and run:
    $ pod init
  3. This will give you a basic Podfile, which you’ll want to edit, to add the following line:
    pod 'CorePlot'
  4. Once you’ve added CorePlot to the Podfile, you can install CorePlot by running:
    pod install
  5. Now, you have to close the project in XCode and open up the newly created workspace for your project. The workspace file has a .xcworkspace extension, and should be in your project directory. Opening the workspace will give you access to your original project, plus all of the CocoaPods you’ve installed in the project.
  6. At this point, if you were writing an Objective C app, you’d be able to start adding CorePlot functionality to your app, but we need to do few things to get it working in Swift.
  7. Create a new header file in XCode, by going to File->New->File…, and select Header File (under iOS->Source). Call your header file <app>-Bridging-Header.h, where <app> is your app’s name. You can read more about bridging headers on Apple’s guide on interoperability.
  8. Add CorePlot to the header file, by adding this line:
    #import "CorePlot-CocoaTouch.h"
  9. Now, you have to tell XCode about the bridging header.
    • Click on the project in the Project navigator
    • Under the Targets heading in the left sidebar, make sure you click on your project’s name
    • Make sure the “All” view is enabled (as opposed to “Basic”)
    • Scroll way down to the “Swift Compiler – Code Generation” section toward the bottom
    • Under Objective-C Bridging Header, enter the name of your bridging header (e.g. <appname>/<appname>-Bridging-Header.h)
    • This should enable access to CorePlot from Swift, let’s try it out.
      Screenshot 2014-08-07 00.34.55
  10. Go into your storyboard and add a plain View object to your view controller. This view will be a placeholder for the graph we’re going to create. You should add proper layout constraints to make sure the view fills the whole screen without going off the screen. Here’s what mine look like (they are all set to zero):
    Screenshot 2014-08-07 00.42.41
  11. Set the view object to have a custom class, and set it to CPTGraphHostingView

    Screenshot 2014-08-07 00.33.37
  12. Create an outlet for the view in your view controller. Call it graphView.
  13. Add the following code to the viewDidLoad method of your ViewController class to create a simple pie chart:
    // create graph
    var graph = CPTXYGraph(frame: CGRectZero)
    graph.title = "Hello Graph"
    graph.paddingLeft = 0
    graph.paddingTop = 0
    graph.paddingRight = 0
    graph.paddingBottom = 0
    // hide the axes
    var axes = graph.axisSet as CPTXYAxisSet
    var lineStyle = CPTMutableLineStyle()
    lineStyle.lineWidth = 0
    axes.xAxis.axisLineStyle = lineStyle
    axes.yAxis.axisLineStyle = lineStyle
    // add a pie plot
    var pie = CPTPieChart()
    pie.dataSource = self
    pie.pieRadius = (self.view.frame.size.width * 0.9)/2
    graph.addPlot(pie)
    self.graphView.hostedGraph = graph
    view raw graph.swift hosted with ❤ by GitHub
  14. Add the CPTPlotDataSource protocol to the list of protocols that your ViewController implements. It should look something like this:
    class ViewController: UIViewController, CPTPlotDataSource {
  15. Add the CPTPlotDataSource methods. The samples below will create four sections in the pie graph, and the size of each section will be equal to its position plus one (i.e. 1, 2, 3, 4), so we’ll get a graph with some variety.
    func numberOfRecordsForPlot(plot: CPTPlot!) -> UInt {
    return 4
    }
    func numberForPlot(plot: CPTPlot!, field fieldEnum: UInt, recordIndex idx: UInt) -> NSNumber! {
    return idx+1
    }
    view raw data.swift hosted with ❤ by GitHub
  16. At this point, you should be able to run your app, and get a properly generated graph:
    Screenshot 2014-08-07 00.45.03

The code for my sample app can be found on GitHub here:

https://github.com/alwold/CorePlotTest

This shows the very basics of how to get up and running. There are some further caveats, especially with calling methods that involve parameters of the NSDecimal type. My solution for that was to add some Objective-C wrapper code. Hopefully, I can show how to do that in a future post.

Update: I have a new post describing how to deal with APIs that use NSDecimal here.