Pie Progress View (iOS and macOS)

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.

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.

Code Review with Git Worktree

I’ve been using a little known feature for some time now and thought it might be worthy of a post for those (like myself) who prefer the Console world of GIT.

Scenario

Para-phrasing from the Git Documentation:

You are in the middle of a refactoring session, unable to commit and you need to checkout another branch to code review, reference or something else. You might typically use git-stash to store your changes away temporarily, however, your working tree is in such a state of disarray that you don’t want to risk disturbing any of it. Use a Git Worktree.

I think we can all agree that this is an all too common scenerio?

Worktree’s are also extremely useful during Code Review. You don’t want to commit your changes, but need to checkout someone elses branch to ensure the work they’ve done actually works. Or perhaps you need to checkout the code to better understand how it works.

Worktree’s

In simple terms, a worktree represents a linked clone of your master worktree. This means its extremely fast to work with and because its generally transient/temporary, you can simply delete it once you’re done.

Workflow

So lets take a look at a typical workflow.

# git worktree add $PATH $BRANCH
# Adds a new worktree at 'path' and performs a checkout of 'branch'
# path:   prs/5.2.0
# branch: release/5.2.0

git worktree add prs/5.2.0 release/5.2.0

Hack away

Simply `cd` into the directory and hack away. The sub-folder is a complete checkout of the branch you specified and has its own `.git` folder. So any changes you make here are affected only on that branch – leaving your original branch untouched.

cd prs/5.2.0``open .

Push and Prune

If you made changes to your worktree, simply push those changes back up to your origin as usual. Then you can remove the sub-folder and prune your worktree from Git.

git push # optional
cd ../..
rm -rf prs # you can remove just the 5.2.0 folder if you have multiple worktree's
git worktree prune

List

Its important to remove your Worktree when done, because Git only allows 1 worktree per branch. If you need to remember where you created your Worktree, you can list them using:

git worktree list

Conclusion

Worktree’s are a great tool to have in your bag. Using this approach I found I’m more likely to checkout someone’s branch during Code Review to ensure it works.