Monday, December 15, 2014

Simple Combinators for Manipulating CGPoint/CGSize/CGRect with Swift

One of the most painful things about Objective-C was having to modify CGPoint, CGSize or CGRect values. The clunky struct interface made even simple modifications verbose and ugly, since struct expressions were read-only:

    CGRect imageBounds = self.view.bounds;
    imageBounds.size.height -= self.footer.bounds.size.height;

    self.imageView.bounds = imageBounds;

Even though we have auto-layout, I often find myself doing this kind of arithmetic with points, size or rects. In Objective-C, it required either generating dummy variables so you can modify members (as above), or really messy struct initialization syntax:

    self.imageView.bounds = (CGRect) { 
        .origin = self.view.bounds.origin,
        .size = CGSizeMake(self.view.bounds.size.width, self.view.bounds.size.height -    
                           self.footer.bounds.size.height) };

Fortunately, none of this boilerplate is necessary with Swift. Since Swift lets you extend even C structures with new methods, I wrote a handful of combinators that eliminate this kind of code. The above snippet can now be replaced with:

    self.imageView.bounds = self.view.bounds.mapHeight { $0 - self.footer.size.height }

I can easily enlarge a scroll view's content size to hold its pages:

    self.scrollView.contentSize = self.scrollView.bounds.size.mapWidth { $0 * CGFloat(pages.count) }

I can do calculations that previously would've required dozens of lines of code in just one or two:

    let topHalfFrame = self.view.bounds.mapHeight { $0 / 2 }
    let bottomHalfFrame = topHalfFrame.mapY { $0 + topHalfFrame.size.height }

These two lines will give me two frames that each take up half of the height of their parent view.

In cases where I simply need to set a value, I use the primitive "with..." functions:

    self.view.bounds.withX(0).withY(0).withSize(0).withHeight(0)

Note that these methods can all be chained to create complex expressions.

The code for these methods is trivial, yet they give you a huge boost in expressive power.

GitHub projecthttps://github.com/moreindirection/SwiftGeometry

Code

extension CGPoint {
    func mapX(f: (CGFloat -> CGFloat)) -> CGPoint {
        return self.withX(f(self.x))
    }
    
    func mapY(f: (CGFloat -> CGFloat)) -> CGPoint {
        return self.withY(f(self.y))
    }
    
    func withX(x: CGFloat) -> CGPoint {
        return CGPoint(x: x, y: self.y)
    }
    
    func withY(y: CGFloat) -> CGPoint {
        return CGPoint(x: self.x, y: y)
    }
}

extension CGSize {
    func mapWidth(f: (CGFloat -> CGFloat)) -> CGSize {
        return self.withWidth(f(self.width))
    }
    
    func mapHeight(f: (CGFloat -> CGFloat)) -> CGSize {
        return self.withHeight(f(self.height))
    }
    
    func withWidth(width: CGFloat) -> CGSize {
        return CGSize(width: width, height: self.height)
    }
    
    func withHeight(height: CGFloat) -> CGSize {
        return CGSize(width: self.width, height: height)
    }
}

extension CGRect {
    func mapX(f: (CGFloat -> CGFloat)) -> CGRect {
        return self.withX(f(self.origin.x))
    }
    
    func mapY(f: (CGFloat -> CGFloat)) -> CGRect {
        return self.withY(f(self.origin.y))
    }
    
    func mapWidth(f: (CGFloat -> CGFloat)) -> CGRect {
        return self.withWidth(f(self.size.width))
    }
    
    func mapHeight(f: (CGFloat -> CGFloat)) -> CGRect {
        return self.withHeight(f(self.size.height))
    }
    
    func withX(x: CGFloat) -> CGRect {
        return CGRect(origin: self.origin.withX(x), size: self.size)
    }
    
    func withY(y: CGFloat) -> CGRect {
        return CGRect(origin: self.origin.withY(y), size: self.size)
    }
    
    func withWidth(width: CGFloat) -> CGRect {
        return CGRect(origin: self.origin, size: self.size.withWidth(width))
    }
    
    func withHeight(height: CGFloat) -> CGRect {
        return CGRect(origin: self.origin, size: self.size.withHeight(height))
    }
}

No comments:

Post a Comment