Skip to content

Instantly share code, notes, and snippets.

@stuartjmoore
Created January 27, 2019 17:42
Show Gist options
  • Save stuartjmoore/a8daef194ee03537750e1a96d6de917f to your computer and use it in GitHub Desktop.
Save stuartjmoore/a8daef194ee03537750e1a96d6de917f to your computer and use it in GitHub Desktop.
Hairline Separator UIView
//
// SeparatorView.swift
//
// Created by Stuart Moore on 1/26/19.
// Copyright © 2019 Stuart J. Moore. All rights reserved.
//
// Adapted from http://www.figure.ink/blog/2016/9/11/hairlines.
// Published by jemmons on 9/11/16.
//
import Foundation
extension CGFloat {
/// The size of a pixel in terms of a point
static var pixel: CGFloat {
return 1 / UIScreen.main.scale
}
// MARK: -
enum RoundingDirection {
case up, down
}
/// Round the receiver to nearest fractional increment
///
/// - Parameters:
/// - fraction: The fraction to round closest to
/// - direction: Whether to round up or down
/// - Returns: The rounded value
func rounded(toNearest fraction: CGFloat, direction: RoundingDirection) -> CGFloat {
let expanded = self / fraction
let rounded = (direction == .down ? floor : ceil)(expanded)
return rounded * fraction
}
}
// MARK: -
import UIKit
/// Renders a view of any size, with a single-pixel wide line
/// along one of the inside edges.
///
/// For example, you can create a view with a frame 1 point tall,
/// but draw a 1 pixel tall line along the top-most edge.
class SeparatorView: UIView {
// MARK: - Types
/// - horizontal: ─
/// - vertical: │
enum Axis {
case horizontal, vertical
}
/// - leading: Left or top
/// - trailing: Right or bottom
enum PixelEdge {
case leading, trailing
}
/// - leading: Left or top
/// - trailing: Right or bottom
struct Insets {
let leading: CGFloat
let trailing: CGFloat
static let zero = Insets(leading: 0, trailing: 0)
}
// MARK: - Properties
/// Interface Builder accessor for `axis`
@IBInspectable var isHorizontal: Bool {
get {
return axis == .horizontal
} set {
axis = (newValue ? .horizontal : .vertical)
}
}
/// Interface Builder accessor for `pointEdge`
@IBInspectable var onLeadingEdge: Bool {
get {
return pixelEdge == .leading
} set {
pixelEdge = (newValue ? .leading : .trailing)
}
}
/// The color of the separator line
/// The background color is drawn behind
@IBInspectable var separatorColor: UIColor = .clear
/// The direction in which to draw the separator
var axis: Axis = .horizontal
/// Which pixel edge inside of a point to draw on
///
/// For example, if you want the separator to butt
/// up against a view below, use `trailing`
var pixelEdge: PixelEdge = .leading
/// Number of points to inset the start and end of the separator
var insets: Insets = .zero
// MARK: - Methods
override func draw(_ rect: CGRect) {
let halfPixel: CGFloat = .pixel / 2
let separator = UIBezierPath()
separator.lineWidth = .pixel
let points: (from: CGPoint, to: CGPoint)
if axis == .horizontal {
let y = (pixelEdge == .leading ? bounds.minY : bounds.maxY)
let adjustedY = (y - halfPixel).rounded(toNearest: .pixel, direction: pixelEdge == .leading ? .up : .down) + halfPixel
points = (from: CGPoint(x: bounds.minX + insets.leading, y: adjustedY), to: CGPoint(x: bounds.maxX - insets.trailing, y: adjustedY))
} else {
let x = (pixelEdge == .leading ? bounds.minX : bounds.maxX)
let adjustedX = (x - halfPixel).rounded(toNearest: .pixel, direction: pixelEdge == .leading ? .up : .down) + halfPixel
points = (from: CGPoint(x: adjustedX, y: bounds.minY + insets.leading), to: CGPoint(x: adjustedX, y: bounds.maxY - insets.trailing))
}
separator.move(to: points.from)
separator.addLine(to: points.to)
separatorColor.setStroke()
separator.stroke()
}
}
@stuartjmoore
Copy link
Author

stuartjmoore commented Jan 27, 2019

Draw hairline (single pixel) separator views without creating fractional constraints. I don’t know, I heard that was bad. Also useful in UIStackViews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment