Skip to content

Instantly share code, notes, and snippets.

@chrislconover
Last active January 25, 2018 18:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrislconover/66aae601888d2339ccd762f210bed1b5 to your computer and use it in GitHub Desktop.
Save chrislconover/66aae601888d2339ccd762f210bed1b5 to your computer and use it in GitHub Desktop.
// Adapted from original Apple Obj-C sample code
import UIKit
import CoreText
import QuartzCore
@IBDesignable
class ClippingLabel : UILabel {
override var text:String? { didSet {
if self.text != oldValue { invalidateIntrinsicContentSize() }}}
override var font:UIFont! { didSet {
if self.font != oldValue { invalidateIntrinsicContentSize() }}}
override func intrinsicContentSize() -> CGSize {
guard let text = text else { return CGSizeZero }
let attributed = NSAttributedString(string: text,
attributes: [NSFontAttributeName:font])
return CTLineGetImageBounds(CTLineCreateWithAttributedString(attributed), nil).size
}
}
@IBDesignable
public class ScalingLabel : UIView {
public override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// MARK: configuration
public var font:UIFont? = UIFont.systemFontOfSize(12)
@IBInspectable public var text:String? {
get { return attributed?.string }
set { guard let t = newValue, f = self.font else { self.attributed = nil; return }
self.attributed = NSAttributedString(string: t, attributes: [NSFontAttributeName:f]) }}
@IBInspectable public var attributed:NSAttributedString? {
get { return self.textLayer.text }
set { self.textLayer.text = newValue; invalidateIntrinsicContentSize() }}
// MARK: layout
override public func layoutSubviews() {
super.layoutSubviews()
self.textLayer.frame = self.layer.bounds
invalidateIntrinsicContentSize()
}
public override func intrinsicContentSize() -> CGSize {
return textLayer.contentFrame.size
}
// private var textLayer:ScalableTextLayer { return self.layer as! ScalableTextLayer }
lazy var textLayer:ScalableTextLayer = {
let layer = ScalableTextLayer()
self.layer.addSublayer(layer)
return layer
}()
}
extension CTFont : Hashable, CustomStringConvertible {
public var hashValue: Int {
return self.description.hashValue
}
public var description:String {
return (CTFontCopyName(self, kCTFontUniqueNameKey) as? NSString as? String) ?? ""
}
}
public func == (left: CTFont, right: CTFont) -> Bool {
return left.hashValue == right.hashValue
}
public class ScalableTextLayer : CALayer {
typealias CTDictionary = [String:AnyObject]
public var text: NSAttributedString? { didSet {
line = self.text != nil ? CTLineCreateWithAttributedString(self.text!) : nil
generatePaths() }}
public var color:CGColor?
public var zoomToFit:Bool = true { didSet { self.setNeedsLayout() }}
public var centerContent:Bool = true { didSet { self.setNeedsLayout() }}
public private(set) var contentFrame:CGRect = CGRectZero
public override init() {
super.init()
geometryFlipped = true
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
geometryFlipped = true
}
func generatePaths() {
CATransaction.begin()
CATransaction.setDisableActions(true)
// clear existing layers, and add new layers as required
for layer in layers { layer.path = nil }
if let line = self.line {
// add new layers as required
var glyphInLine = 0
let glyphCount = CTLineGetGlyphCount(self.line)
for _ in layers.count..<glyphCount { layers.append(createShapeLayer()) }
let offset = CTLineGetImageBounds(line, nil).origin
// for each run
let runs = CTLineGetGlyphRuns(line) as [AnyObject] as! [CTRun]
for (runIndex, run) in runs.enumerate() {
// get font
let attributes = CTRunGetAttributes(run) as NSDictionary as! [String:AnyObject]
let runFont:CTFont = attributes[kCTFontAttributeName as String] as! CTFont
let style = styleFromAttributes(attributes)
// get glyphs for current run
let glyphCount = CTRunGetGlyphCount(run)
let range = CFRangeMake(0, glyphCount)
var glyphs = Array<CGGlyph>(count: glyphCount, repeatedValue: CGGlyph())
var positions = [CGPoint](count: glyphCount, repeatedValue: CGPointZero)
CTRunGetGlyphs(run, range, &glyphs)
CTRunGetPositions(run, range, &positions)
// for each glyph in run
for (i, glyph) in glyphs.enumerate() {
// get next glyph
let layer = layers[glyphInLine]
glyphInLine += 1
layer.style = style
layer.path = pathForGlyph(glyph, withFont: runFont)
layer.name = "Font \(runFont), run: \(runIndex), glyph \(i): \(glyph)"
var position = positions[i]
position = CGPoint(x: position.x - offset.x, y: position.y - offset.y)
layer.position = position
layer.backgroundColor = nil
// let glyphFrame = withUnsafePointer(&glyph) { pointer -> CGRect in
// return CTFontGetBoundingRectsForGlyphs(runFont, .Default, pointer, nil, 1) }
//
// let boundingLayer = CALayer()
// boundingLayer.frame = glyphFrame
// boundingLayer.borderColor = UIColor.greenColor().CGColor
// boundingLayer.borderWidth = 0.1
// layer.addSublayer(boundingLayer)
}
}
CATransaction.commit()
self.setNeedsLayout()
}
func styleFromAttributes(attributes: [String:AnyObject]) -> [String:AnyObject] {
// return fill color, if specified
return ["fillColor": color
?? attributes[NSForegroundColorAttributeName]
?? UIColor.blackColor().CGColor]
}
func pathForGlyph(glyph:CGGlyph, withFont font: CTFont) -> CGPath? {
// case of existing font entry
if var glyphToPath = ScalableTextLayer.fontToGlyphToPath[font] {
let cachePath: (CGGlyph, CTFont) -> CGPath? = { glyph, font in
let path = CTFontCreatePathForGlyph(font, glyph, nil)
glyphToPath[glyph] = path
return path }
return glyphToPath[glyph] ?? cachePath(glyph, font)
}
// case of missing font, create and return font/path
else {
if let path = CTFontCreatePathForGlyph(font, glyph, nil) {
ScalableTextLayer.fontToGlyphToPath[font] = [glyph:path]
return path
}
return nil
}
}
override public func layoutSublayers() {
if let line = self.line {
self.sublayers!.count
var transform = CATransform3DIdentity
// if we are to scale
if zoomToFit {
let lineBounds = CTLineGetImageBounds(line, nil)
let lineWidth = lineBounds.size.width
let lineHeight = lineBounds.size.height
// layer geometry
let bounds = self.bounds
let anchorPoint = self.anchorPoint
let scaleX = bounds.size.width / lineWidth
let scaleY = bounds.size.height / lineHeight
let anchorX = bounds.origin.x + bounds.size.width * anchorPoint.x;
let anchorY = bounds.origin.y + bounds.size.height * anchorPoint.y;
// translate to origin so that we can properly scale
transform = CATransform3DTranslate(transform, -anchorX, -anchorY, 0.0)
// // case of smaller x scale
// if scaleX < scaleY {
// transform = CATransform3DScale(transform, scaleX, scaleX, 1.0)
// if centerContent {
// let ty = ((bounds.size.height / 2.0) / scaleX) - lineHeight / 2.0
// transform = CATransform3DTranslate(transform, 0.0, ty, 0.0)
// }
// }
// case of smaller y scale
// else {
transform = CATransform3DScale(transform, scaleY, scaleY, 1.0)
if centerContent {
let tx = ((bounds.size.width / 2.0) / scaleY) - lineWidth / 2.0
transform = CATransform3DTranslate(transform, tx, 0, 0.0)
}
// update content frame to reflect curtains or letterboxing
contentFrame = CGRectMake(lineBounds.origin.x * scaleY, lineBounds.origin.y * scaleY,
lineBounds.size.width * scaleY, lineBounds.size.height * scaleY)
// }
// translate back to original anchor point
transform = CATransform3DTranslate(transform, anchorX, anchorY, 0.0)
}
// apply tranform to all sub layers
CATransaction.begin()
CATransaction.setDisableActions(true)
self.sublayerTransform = transform
CATransaction.commit()
}
}
func createShapeLayer() -> CAShapeLayer {
let layer = CAShapeLayer()
self.addSublayer(layer)
layer.geometryFlipped = false
return layer
}
var line: CTLine!
var layers: [CAShapeLayer!] = []
typealias GlyphToPath = Dictionary<CGGlyph, CGPath>
typealias FontToGlyphToPath = Dictionary<CTFont, GlyphToPath>
static var fontToGlyphToPath = FontToGlyphToPath()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment