Pie Progress View (iOS and macOS)

For more on this subject, check out my related post:
https://152percent.com/blog/2018/5/3/ios-t...

Following on from my previous post about cross platform development, I wanted to showcase a simple view/layer implementation I recently built for an app I'm working on.

Pie Progress.gif

The design requires a pie chart to represent progress. I initially built it using CAShapeLayer however this quickly proved to be the wrong approach since the shape layer would attempt to 'morph' between values, rather than simply adjust the chart.

At this point I decided to move to a more custom approach. I also knew this would lend itself nicely to cross platform development, since layer's are generally identical across both iOS and macOS.

I plan on doing some more in-depth posts that go into the deeper details of making a cross-platform application, but for the purposes of this post I'll keep things short.

Cross Platform

I generally write the code for one platform first, one file at a time. Then assess the requirements to work out any extensions, typealias's, etc.. I may need.

On iOS its relatively simple to get a cgPath from a UIBezierPath however on macOS, we need to do this ourselves. So I wrote an equivalent extension.

#if os(OSX)
import AppKit

public extension NSBezierPath {
    var cgPath: CGPath { /* implementation */ }
}
#endif

Next up, I needed some typealias's to make it easier to use a single call throughout my code.

#if os(OSX)
    import AppKit
    public typealias Color = NSColor
    public typealias View = NSView

    private func scale() -> CGFloat {
        return NSScreen.main?.backingScaleFactor ?? 1
    }

    extension NSView {
        internal var nonOptionalLayer: CALayer {
            return layer!
        }
    }
#else
    import UIKit
    public typealias Color = UIColor
    public typealias View = UIView

    internal func scale() -> CGFloat {
        return UIScreen.main.scale
    }

    extension UIView {
        internal var nonOptionalLayer: CALayer {
            return layer
        }
    }
#endif

Coordinate System

One of the most interesting differences between iOS and macOS development, is that the y-coordinates are inverted. Where iOS expects the origin to be top-left, macOS expects the origin to be bottom-left.

To normalise the origin for both platforms, NSView provides a simple isFlipped property. Returning true for this property, moves the origin to the top-left, matching what we'd expect on iOS.

public override var isFlipped: Bool {
    return true
}

Progress View

The code itself is extremely simple. We simply subclass CALayer to provide our own drawing for each frame. This allows us to animate the progress when its value changes. We even get all the benefits of implicit animation, etc...

I then wrapped the layer with a custom (UI/NS)View, added some IBDesignable support and some configurable properties to make it easier to work with.

Lastly I added a setProgress(progress:animated:) function which was as simple as disabling the implicit animation when animated == false.

public func setProgress(_ progress: CGFloat, animated: Bool) {
    guard animated else {
        CATransaction.setDisableActions(true)
        self.progress = progress
        CATransaction.setDisableActions(false)
        return
    }

    self.progress = progress
}

Summary

Building a cross platform component isn't too difficult. Focusing on UI components at this level, allows the greatest amount of reuse as well. Adding storyboard/XIB support also aides the user of the component.

I've included a link to the repo, which contains an example Xcode project with both iOS and macOS targets–as well as a iOS Playground.


If you liked this post or want to discuss more, leave a comment below or find me on Twitter