Created
January 27, 2019 17:42
-
-
Save stuartjmoore/a8daef194ee03537750e1a96d6de917f to your computer and use it in GitHub Desktop.
Hairline Separator UIView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Draw hairline (single pixel) separator views without creating fractional constraints. I don’t know, I heard that was bad. Also useful in
UIStackView
s.