Different and inconsistent line heights in NSTextView and CATextLayer - swift

I am working on creating a canvas-like interface where text box objects can be created and edited. All drawing is handled through CALayers, but I am using invisible NSTextViews for event handling when a text box is being actively edited. For accurate event capture, I need the lines of text in the NSTextView and CATextLayer to be laid out identically (particularly for multi-line text boxes).
The problem is that NSTextView and CATextLayer seem to render text slightly differently, giving them different line heights. This varies by font; sometimes NSTextView comes up shorter, sometimes longer. The black text below is in an NSTextView, while the blue is a CATextLayer. Helvetica Neue is slightly misaligned as lines accumulate, while Helvetica is quite overtly misaligned from the start.
I have no idea where the difference comes from, especially since the effect is so unpredictable. Is there a way to account for the line height difference, or, is there a better way to relay events captured from an NSTextView to/from a CATextLayer?
Swift playground code (macOS 10.12.1, Xcode 7.1.1; though the issue has existed since at least 10.11):
// Tested fonts:
// Andale Mono - slight misalignment
// Courier - extreme misalignment
// Courier New - small misalignment
// Helvetica - large misalignment
// Helvetica Neue - no (?) misalignment
import Cocoa
// setup
var str = "Text\nText\nText\nText\nText\nText\nText\nText\nText"
var theFont = CTFontCreateWithName("Helvetica Neue", 20, nil)
var parentView:NSView = NSView(frame: NSRect(origin: CGPointZero, size: CGSize(width: 300, height: 300)))
// create basic NSTextView
var textView:NSTextView = NSTextView(frame: NSRect(origin: CGPointZero, size: CGSize(width: 100, height: 300)))
textView.string = str
textView.font = theFont
parentView.addSubview(textView)
// create what SHOULD be an equivalent CATextLayer
var layerView:NSView = NSView(frame: NSRect(origin: CGPoint(x: 100, y: 0), size: CGSize(width: 100, height: 300)))
layerView.wantsLayer = true
var textLayer = CATextLayer()
textLayer.contentsScale = 2
textLayer.backgroundColor = NSColor(calibratedHue: 0, saturation: 0, brightness: 1, alpha: 1).CGColor
textLayer.foregroundColor = NSColor(calibratedHue: 0.6, saturation: 1, brightness: 1, alpha: 1).CGColor
textLayer.string = str
textLayer.font = CTFontCopyFullName(theFont)
textLayer.fontSize = CTFontGetSize(theFont)
layerView.layer = textLayer
parentView.addSubview(layerView)
// final display
parentView

Related

textfield bottom line issue in swift

I am using the following code in Swift 4.2 to have Textfield bottom border:
extension UITextField {
func useUnderline() {
let border = CALayer()
let borderWidth = CGFloat(1.0)
border.borderColor = UIColor.lightGray.cgColor
border.frame = CGRect(origin: CGPoint(x: 0,y :self.frame.size.height - borderWidth), size: CGSize(width: self.frame.size.width, height: self.frame.size.height))
border.borderWidth = borderWidth
self.layer.addSublayer(border)
self.layer.masksToBounds = true
}
}
but in different model of iPhones I get a different behaviors. for example:
in Iphone XR:
and with iPhone X or 8:
Any solution to this issue would be appreciated.
I'm sure this problem does not depend on an iPhone model, but from the place where you call useUnderline(). If you will call useUnderline() from viewDidAppear, then the line will draw correct. Because the size of UIControls not yet actual when calling viewDidLoad and viewWillAppear.
But keep in your mind you will have a problem with your extension because sublayers will be added to UITextField on every viewDidAppear execution.
This could be a prime example where you must use a UITextField subclass instead of an extension. The accepted answer will work, but as suggested there, your TextField will get many layers added to it as the user uses his application.
The below subclass could solve this issue:
class CustomTextField: UITextField{
let border = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
useUnderline()
}
func useUnderline(){
if layer.sublayers?.contains(border) ?? false{
border.removeFromSuperlayer()
}
border.path = UIBezierPath.init(rect: CGRect.init(x: 0, y: self.bounds.height - 2, width: self.bounds.width, height: 2)).cgPath
border.fillColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.3).cgColor
self.layer.insertSublayer(border, at: 10)
}
}

Swift: Center NSAttributedString in View horizontally and vertically

Hi there,
I have a class CardButton: UIButton . In the draw method of this CardButton, I would like to add and center(vertically and horizontally) NSAttributed String, which is basically just one Emoji, inside of it. The result would look something like this:
However, NSAttributedString can be only aligned to center in horizontal dimension inside the container.
My idea for solution:
create a containerView inside of CardButton
center containerView both vertically and horizontally in it's container(which is CardButton)
add NSAttributedString inside the containerView and size containerView to fit the string's font.
So the result would look something like this:
My attempt for this to happen looks like this:
class CardButton: UIButton {
override func draw(){
//succesfully drawing the CardButton
let stringToDraw = attributedString(symbol, fontSize: symbolFontSize) //custom method to create attributed string
let containerView = UIView()
containerView.backgroundColor = //green
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
let centerXConstraint = containerView.centerXAnchor.constraint(equalTo: (self.centerXAnchor)).isActive = true
let centerYConstraint = containerView.centerYAnchor.constraint(equalTo: (self.centerYAnchor)).isActive = true
stringToDraw.draw(in: containerView.bounds)
containerView.sizeToFit()
}
}
Long story short, I failed terribly. I first tried to add containerView to cardButton, made the background green, gave it fixed width and height jut to make sure that it got properly added as a subview. It did. But once I try to active constraints on it, it totally disappears.
Any idea how to approach this?
There is always more than one way to achieve any particular UI design goal, but the procedure below is relatively simple and has been adapted to suit the requirements as presented and understood in your question.
The UIButton.setImage method allows an image to be assigned to a UIButton without the need for creating a container explicitly.
The UIGraphicsImageRenderer method allows an image to be made from various components including NSAttributedText, and a host of custom shapes.
The process utilising these two tools to provide the rudiments for your project will be to:
Render an image with the appropriate components & size
Assign the rendered image to the button
A class could be created for this functionality, but that has not been explored here.
Additionally, your question mentions that when applying constraints the content disappears. This effect can be observed when image dimensions are too large for the container, constraints are positioning the content out of view and possibly a raft of other conditions.
The following code produces the above image:
func drawRectangleWithEmoji() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { (ctx) in
// Create the outer square:
var rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 7.5, dy: 7.5)
var roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 50).cgPath
// MARK: .cgPath creates a CG representation of the path
ctx.cgContext.setFillColor(UIColor.white.cgColor)
ctx.cgContext.setStrokeColor(UIColor.blue.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Create the inner square:
rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 180, dy: 180)
roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 10).cgPath
ctx.cgContext.setFillColor(UIColor.green.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Add emoji:
var fontSize: CGFloat = 144
var attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize)//,
//.backgroundColor: UIColor.gray //uncomment to see emoji background bounds
]
var string = "❤️"
var attributedString = NSAttributedString(string: string, attributes: attrs)
let strWidth = attributedString.size().width
let strHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - strWidth / 2, y: 256 - strHeight / 2))
// Add NSAttributedString:
fontSize = 56
attrs = [
.font: UIFont.systemFont(ofSize: fontSize),
.foregroundColor: UIColor.brown
]
string = "NSAttributedString"
attributedString = NSAttributedString(string: string, attributes: attrs)
let textWidth = attributedString.size().width
let textHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - textWidth / 2, y: 384 - textHeight / 2))
}
return img
}
Activate the NSLayoutContraints and then the new image can be set for the button:
override func viewDidLoad() {
super.viewDidLoad()
view = UIView()
view.backgroundColor = .white
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
// buttonsView.backgroundColor = UIColor.gray
view.addSubview(buttonsView)
NSLayoutConstraint.activate([
buttonsView.widthAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.heightAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
let cardButton = UIButton(type: .custom)
cardButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
cardButton.setImage(drawRectangleWithEmoji(), for: .normal)
let frame = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth)
cardButton.frame = frame
buttonsView.addSubview(cardButton)
}
Your comments will be appreciated as would constructive review of the code provided.

In SceneKit, putting a text label onto SCNGeometry or SCNBox

I'm trying to put a simple text label onto one (hopefully all types) of built in SCNGeometry shapes as they move across the screen. The closest I have come to success is adding a CALayer with CATextLayer to a SCNBox via .firstMaterial.diffuse.contents, as described in this thread.
BUT, the text is never readable. With a SCNBox of height 1.0: when the size of the layer.frame and textLayer.fontSize is 1.0, the text does not appear; as the frame and font size increase (not the box) the text appears blotchy, like in the image below; and when very large, the text appears as squiggly lines.
The following code is part of the method that spawns shapes:
var geometry:SCNGeometry
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 4, height: 4)
layer.backgroundColor = UIColor.white.cgColor
var textLayer = CATextLayer()
textLayer.frame = layer.bounds
textLayer.fontSize = layer.bounds.size.height
textLayer.string = "Matilda"
textLayer.alignmentMode = kCAAlignmentLeft
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.display()
layer.addSublayer(textLayer)
let geometry = SCNBox(width: 1.0,
height: 1.0,
length: 3.0,
chamferRadius: 0.0)
geometry.firstMaterial?.locksAmbientWithDiffuse = true
geometry.firstMaterial?.diffuse.contents = layer
let geometryNode = SCNNode(geometry: geometry)
geometryNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
scnScene.rootNode.addChildNode(geometryNode)
As pointed out by David R., the units of the box size is not the same as the units of the frame size.
It worked after adjusting the frame to (and optimising SCNBox size):
layer.frame = CGRect(x: 0, y: 0, width: 200, height: 50)

Writing in NSView

I am trying to render text in a NSView canvas. I need to write three lines of text and ignore what's beyond. String.draw(in:withAttributes) with a provided rect seems perfect to do it. My code looks like this:
func renderText(_ string:String, x:Double, y:Double, numberOfLines: Int, withColor color:Color) -> Double {
let font = NSFont.boldSystemFont(ofSize: 11)
let lineHeight = Double(font.ascender + abs(font.descender) + font.leading)
let textHeight = lineHeight * Double(numberOfLines) + font.leading // three lines
let textRect = NSRect(x: x, y: y, width: 190, height: textHeight)
string.draw(in: textRect, withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: color])
return textHeight
}
renderText("Lorem ipsum...", x: 100, y: 100, numberOfLines: 3, withColor: NSColor.white)
Without adjustments, I get only two lines of text rendered:
I am following these guidelines: https://developer.apple.com/library/content/documentation/TextFonts/Conceptual/CocoaTextArchitecture/FontHandling/FontHandling.html#//apple_ref/doc/uid/TP40009459-CH5-SW18
I am missing something?
Ultimately your text makes it to the screen by calling upon the classes that comprise Cocoa's text architecture, so it makes sense to get information about line height directly from these classes. In the code below I've created an NSLayoutManager instance, and set its typesetter behaviour property to match the value of the typesetter that is ultimately used by the machinery created by the function drawInRect:withAttributes:. Calling the layout manager's defaultLineHeight method then gives you the height value you're after.
lazy var layoutManager: NSLayoutManager = {
var layoutManager = NSLayoutManager()
layoutManager.typesetterBehavior = .behavior_10_2_WithCompatibility
return layoutManager
}()
func renderText(_ string:String, x:Double, y:Double, numberOfLines: Int, withColor color:NSColor) -> Double {
let font = NSFont.boldSystemFont(ofSize: 11)
let textHeight = Double(layoutManager.defaultLineHeight(for: font)) * Double(numberOfLines)
let textRect = NSRect(x: x, y: y, width: 190, height: textHeight)
string.draw(in: textRect, withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: color])
return textHeight
}
You are writing your text into a precise typographic bounds, but the system may adjust the size of the text for on-screen presentation to make the text more legible (e.g. substitute screen fonts for for vector fonts). Using an exact typographic bounds can also cause problems for characters with ascenders or descenders that fall outside of bounds. For example the "A-ring" character or a capital E with an grave accent.
To find the bounds of text using rules of the CGContext that it will be drawn in I suggest boundingRectWithSize:options:context: (for NSAttributedString) and boundingRectWithSize:options:attributes:context: (for NSString)
I would try adding a small delta to textHeight -- Doubles are perfectly accurate.

Superimpose a user filled out UITextView over an image with CGRect, not the same as combining two images?

I've been using this function to combine two images together using CG rect:
let renderer = UIGraphicsImageRenderer(size: CGSize(width: frame.bounds.size.width, height: frame.bounds.size.height))
img = renderer.image { ctx in
let someSize = CGSize(width: imageView.bounds.size.width, height: imageView.bounds.size.height)
currentImage.scaleImageToSize(newSize: someSize).draw(in: CGRect(x: 0, y: 35, width: frame.bounds.size.width, height: imageView.bounds.size.height))
//textView?.draw(CGRect(x: 0, y: 0, width: textView.bounds.size.width, height: textView.bounds.size.height))
frames = UIImage(named: framesAr)
frames?.draw(in: CGRect(x: 0, y: 0, width: frame.bounds.size.width, height: frame.bounds.size.height))
}
I use this CGRect function to combines the currentImage (UIImage) and frame(UIImage) into a single UIImage no problem, but my attempts to add in a text view in the same fashion doesn't seem to be working.
I have a UITextView in front of an image for testing purposes. My hope was to be able to type text in the textView and use the commented out line in the above function to draw the entire textView and its contents in place.
When the image is saved the function runs, but the textView is simply ignored. Googling yields results on how to add strings to images, but not a user fillable textview in this fashion.
Any help would be greatly appreciated!
I founds a way that works, but its more of a workaround than anything else.
setup your textview like ichamps answer from this question:
How do I get text from a Swift textView?
then print the contents of the textview onto the image like this:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
let attrs = [NSFontAttributeName: UIFont(name: "Avenir book", size: 14)!, NSParagraphStyleAttributeName: paragraphStyle]
let string = "\(myString)"
string.draw(with: CGRect(x: 0, y: 0, width: frame.bounds.size.width, height: frame.bounds.size.height), options: .usesLineFragmentOrigin, attributes: attrs, context: nil)