How to remove labels from after drawing with drawrect() - ios

I am pretty new to drawing programmatically. I've managed to draw a line graph, complete with points, lines, auto-scaling axes, and axis labels. In other screens, I can change characteristics of the graph and when I return to the screen, I refresh it with setNeedsDisplay() in the viewWillAppear function of the containing viewController. The lines are redrawn perfectly when I do this.
The new data that is added in other screens may require rescaling the axes. The problem is that when the graph is redrawn, the number labels on the axes are just added to the graph, without removing the old ones, meaning that some labels may be overwritten, while some old ones just remain there next to the new ones.
I think I see why this happens, in that I am creating a label and adding a subview, but not removing it. I guess I figured that since the lines are erased and redrawn, the labels would be, too. How do I cleanly relabel my axes? Is there a better way to do this? My function for creating the labels is listed below. This function is called by drawRect()
func createXAxisLabels(interval: Float, numIntervals: Int) {
let xstart: CGFloat = marginLeft
let yval: CGFloat = marginTop + graphHeight + 10 // 10 pts below the x-axis
var xLabelVals : [Float] = [0]
var xLabelLocs : [CGFloat] = [] // gives the locations for each label
for i in 0...numIntervals {
xLabelLocs.append(xstart + CGFloat(i) * graphWidth/CGFloat(numIntervals))
xLabelVals.append(Float(i) * interval)
}
if interval >= 60.0 {
xUnits = "Minutes"
xUnitDivider = 60
}
for i in 0...numIntervals {
let label = UILabel(frame: CGRectMake(0, 0, 50.0, 16.0))
label.center = CGPoint(x: xLabelLocs[i], y: yval)
label.textAlignment = NSTextAlignment.Center
if interval < 1.0 {
label.text = "\(Float(i) * interval)"
} else {
label.text = "\(i * Int(interval/xUnitDivider))"
}
label.font = UIFont.systemFontOfSize(14)
label.textColor = graphStructureColor
self.addSubview(label)
}
}

drawRect should just draw the rectangle, not change the view hierarchy. It can be called repeatedly. Those other labels are other views and do their own drawing.
You have a couple of options
Don't use labels -- instead just draw the text onto the rect.
-or-
Add/remove the labels in the view controller.
EDIT (from the comments): The OP provided their solution
I just replaced my code inside the for loop with:
let str : NSString = "\(xLabelVals[i])"
let paraAttrib = NSMutableParagraphStyle()
paraAttrib.alignment = NSTextAlignment.Center
let attributes = [
NSFontAttributeName: UIFont.systemFontOfSize(14),
NSForegroundColorAttributeName: UIColor.whiteColor(),
NSParagraphStyleAttributeName: paraAttrib]
let xLoc : CGFloat = CGFloat(xLabelLocs[i] - 25)
let yLoc : CGFloat = yval - 8.0
str.drawInRect(CGRectMake(xLoc, yLoc, 50, 16), withAttributes: attributes)

Related

SKLabelNode not respecting zPosition?

I have two SKLabelNodes, one which displays the score and another which acts as a shadow behind the score. Every time my player passes through a goal I update the score labels to increment by 1 point by calling the updateScoreLabels() method below.
But for some reason no matter what zPosition I set for each of these SKLabelNodes, the two nodes are drawn in a random order and every time I call the updateScoreLabels() method their order flips randomly. It also doesn't seem to matter what I have .ignoresSiblingOrder set to or make a difference if I add one SKLabelNode as a child of the other. The nodes just ignore zPosition altogether. What am I doing wrong? Here is my code:
This method is called in didMoveToView():
func createScoreLabel() {
//Configure score label
scoreLabel = SKLabelNode(fontNamed: "8BIT WONDER Nominal")
scoreLabel.text = "O"
scoreLabel.fontSize = 32
scoreLabel.position = CGPoint(x: -UIScreen.main.bounds.size.width/2 - scoreLabel.frame.size.width/2, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60)
scoreLabel.zPosition = 999
//Configure shadow
scoreLabelShadow.attributedText = NSAttributedString(string: "O", attributes: scoreLabelShadowAttributes)
scoreLabelShadow.position = CGPoint(x: -UIScreen.main.bounds.size.width/2 - scoreLabel.frame.size.width/2, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60)
scoreLabelShadow.zPosition = -9999999999
//Add labels
cam.addChild(scoreLabel)
cam.addChild(scoreLabelShadow)
//Animate labels
scoreLabel.run(SKAction.move(to: CGPoint(x: -UIScreen.main.bounds.size.width/2 + scoreLabel.frame.size.width/2 + 30, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60), duration: 0.1))
scoreLabelShadow.run(SKAction.move(to: CGPoint(x: -UIScreen.main.bounds.size.width/2 + scoreLabel.frame.size.width/2 + 30, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60), duration: 0.1))
}
and then this is called when I increment the score labels. Whenever this is called the zPositions flip randomly:
func updateScoreLabel() {
let formattedScore = String(score).replacingOccurrences(of: "0", with: "O")
scoreLabel.text = String(formattedScore)
scoreLabelShadow.attributedText = NSAttributedString(string: formattedScore, attributes: scoreLabelShadowAttributes)
}
As I answered here: I usually create enums to establish zPositions, so I can simply manage the different layers:
enum ZPositions: Int {
case background
case foreground
case player
case otherNodes
}
“case background” is the lower position, it’s equal to the default position of 0;
“case foreground” = 1
“case player” = 2
“case other nodes” = 3
So, when you setup a new item, you can give it the zPosition simply this way:
button.zPosition = CGFloat(ZPosition.otherNodes.rawValue)
(button now has the highest zPosition) Hope it helps...
Try to reset zPositions each time you update labels see if that resolves.
func updateScoreLabel() {
let formattedScore = String(score).replacingOccurrences(of: "0", with: "O")
scoreLabel.text = String(formattedScore)
scoreLabelShadow.attributedText = NSAttributedString(string: formattedScore, attributes: scoreLabelShadowAttributes)
scoreLabel.zPosition = 999
scoreLabelShadow.zPosition = -9999999999
}
Also, having zPosition range of 999 to -9999999999 is not a good practice which could be the cause of the problem. I highly doubt, you have that many layers in your SKSCene.
Store and re-use your layers' zPositions in a Singleton instance and have a categorical system ie: zPosHUD:CGFloat = 10, zPosPlayer:CGFloat = 3, zPosBackground:CGFloat = -5 etc. This will help you be more organized and eventually less bugs will occur in your application.
Hope this helps.

Aligning glyphs to the top of a UITextView after sizeToFit

The app I'm working on supports hundreds of different fonts. Some of these fonts, particularly the script fonts, have significant ascenders and descenders. When sizeToFit() is called on a UITextView with some of these fonts, I end up with significant top and bottom padding (the image on the left). The goal is to end up with the image on the right, such that the tallest glyph is aligned flush with the top of the text view's bounding box.
Here's the log for the image above:
Point Size: 59.0
Ascender: 70.21
Descender: -33.158
Line Height: 103.368
Leading: 1.416
TextView Height: 105.0
My first thought was to look at the height of each glyph in the first line of text, and then calculate the offset between the top of the container and the top of the tallest glyph. Then I could use textContainerInset to adjust the top margin accordingly.
I tried something like this in my UITextView subclass:
for location in 0 ..< lastGlyphIndexInFirstLine {
let glphyRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: location, length: 1), in: self.textContainer)
print(glphyRect.size.height) // prints 104.78399999999999 for each glyph
}
Unfortunately, this doesn't work because boundRect(forGlyphRange:in:) doesn't appear to return the rect of the glyph itself (I'm guessing this is always the same value because it's returning the height of the line fragment?).
Is this the simplest way to solve this problem? If it is, how can I calculate the distance between the top of the text view and the top of the tallest glyph in the first line of text?
This doesn't appear to be possible using TextKit, but it is possible using CoreText directly. Specifically, CGFont's getGlyphBBoxes returns the correct rect in glyph space units, which can then be converted to points relative to the font size.
Credit goes to this answer for making me aware of getGlyphBBoxes as well as documenting how to convert the resulting rects to points.
Below is the complete solution. This assumes you have a UITextView subclass with the following set beforehand:
self.contentInset = .zero
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0.0
This function will now return the distance from the top of the text view's bounds to the top of the tallest used glyph:
private var distanceToGlyphs: CGFloat {
// sanity
guard
let font = self.font,
let fontRef = CGFont(font.fontName as CFString),
let attributedText = self.attributedText,
let firstLine = attributedText.string.components(separatedBy: .newlines).first
else { return 0.0 }
// obtain the first line of text as an attributed string
let attributedFirstLine = attributedText.attributedSubstring(from: NSRange(location: 0, length: firstLine.count)) as CFAttributedString
// create the line for the first line of attributed text
let line = CTLineCreateWithAttributedString(attributedFirstLine)
// get the runs within this line (there will typically only be one run when using a single font)
let glyphRuns = CTLineGetGlyphRuns(line) as NSArray
guard let runs = glyphRuns as? [CTRun] else { return 0.0 }
// this will store the maximum distance from the baseline
var maxDistanceFromBaseline: CGFloat = 0.0
// iterate each run
for run in runs {
// get the total number of glyphs in this run
let glyphCount = CTRunGetGlyphCount(run)
// initialize empty arrays of rects and glyphs
var rects = Array<CGRect>(repeating: .zero, count: glyphCount)
var glyphs = Array<CGGlyph>(repeating: 0, count: glyphCount)
// obtain the glyphs
self.layoutManager.getGlyphs(in: NSRange(location: 0, length: glyphCount), glyphs: &glyphs, properties: nil, characterIndexes: nil, bidiLevels: nil)
// obtain the rects per-glyph in "glyph space units", each of which needs to be scaled using units per em and the font size
fontRef.getGlyphBBoxes(glyphs: &glyphs, count: glyphCount, bboxes: &rects)
// iterate each glyph rect
for rect in rects {
// obtain the units per em from the font ref so we can convert the rect
let unitsPerEm = CGFloat(fontRef.unitsPerEm)
// sanity to prevent divide by zero
guard unitsPerEm != 0.0 else { continue }
// calculate the actual distance up or down from the glyph's baseline
let glyphY = (rect.origin.y / unitsPerEm) * font.pointSize
// calculate the actual height of the glyph
let glyphHeight = (rect.size.height / unitsPerEm) * font.pointSize
// calculate the distance from the baseline to the top of the glyph
let glyphDistanceFromBaseline = glyphHeight + glyphY
// store the max distance amongst the glyphs
maxDistanceFromBaseline = max(maxDistanceFromBaseline, glyphDistanceFromBaseline)
}
}
// the final top margin, calculated by taking the largest ascender of all the glyphs in the font and subtracting the max calculated distance from the baseline
return font.ascender - maxDistanceFromBaseline
}
You can now set the text view's top contentInset to -distanceToGlyphs to achieve the desired result.

Draw circle behind particular characters of UILabel

I am trying to draw circle behind particular parts of UILabel characters without having to use several labels or recompute frames.
I know that I can insert an image into UILabel using attributed text but I want the circle to stand behind the numbers (from 1 to 9). I don't want to store images from 1 to 9.
I also cannot find any pod achieving this.
let fullText = NSMutableAttributedString()
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(named: "\(number)_\(color == .yellow ? "yellow" : "gray")")
let imageString = NSAttributedString(attachment: imageAttachment)
let endString = NSAttributedString("nouveaux")
fullString.append(imageString)
fullString.append(endString)
mainLabel.attributedText = fullString
EDIT
I want to draw circle behind each digit of a string.
I would like it to be a generic tool if possible, some kind of parser that returns an attributed string if possible, a unique UIView if not.
Another use case is "4 messages | chat 2" (4 being yellow, 2 being gray)
I don't understand why you don't just use a utility function that draws the digit-in-a-circle in real time. That way, you can have any digit font, circle size, digit color, and circle color that you like. For example:
func circleAroundDigit(_ num:Int, circleColor:UIColor,
digitColor:UIColor, diameter:CGFloat,
font:UIFont) -> UIImage {
precondition((0...9).contains(num), "digit is not a digit")
let p = NSMutableParagraphStyle()
p.alignment = .center
let s = NSAttributedString(string: String(num), attributes:
[.font:font, .foregroundColor:digitColor, .paragraphStyle:p])
let r = UIGraphicsImageRenderer(size: CGSize(width:diameter, height:diameter))
return r.image {con in
circleColor.setFill()
con.cgContext.fillEllipse(in:
CGRect(x: 0, y: 0, width: diameter, height: diameter))
s.draw(in: CGRect(x: 0, y: diameter / 2 - font.lineHeight / 2,
width: diameter, height: diameter))
}
}
Here's how to use it:
let im = circleAroundDigit(3, circleColor: .yellow,
digitColor: .white, diameter: 30, font:UIFont(name: "GillSans", size: 20)!)
Here's the resulting image; note, though, that every aspect of this can be tweaked just by calling the function differently:
Okay, so now you've got an image. You say you already know how to insert an image inline into an attributed string and display that as part of the label, so I won't explain how to do that:
This is pretty straightforward using a UIStackView, setting the corner radius of the coloured view's layer…
result…
or with 2 stack views in an outer stack view≥
result…

label creation not positioned in the dwg

I have a function that gets called to create labels and position them to simulate dimensions in a drawing (like a cad front view); however they all get positioned in the top left even though the x and y coordinates are being sent correctly via the call function. I've hit a wall on this one and would appreciate any help.see screenshot I took and you'll notice that all the labels are scrunched up in the top left corner
All the dwg gets added to the CGContext
Here's my function:
public func drawText(ctx: CGContext,
txtCol:UIColor,
text:String,
posX:Double,
posY:Double,
rotation:Double,
fill:Bool) -> CGContext {
let label = UILabel()
label.textAlignment = .center
label.text = text
label.textColor = txtCol
label.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
if fill {
label.backgroundColor = UIColor.yellow
}
label.draw(CGRect(x: CGFloat(posX), y: CGFloat(posY), width: 100, height: 30))
label.layer.render(in: ctx)
return ctx
}
As renderInContext: documentation states : "Renders in the coordinate space of the layer." This means the frame origin of your layer/view is ignored.
You might want to use CGContextTranslateCTM before calling label.layer.render(in: ctx). Remember to save and restore the CTM on each call using CGContextSaveGState and CGContextRestoreGState.
Also, I doubt the need to call label.draw(CGRect(x: CGFloat(posX), y: CGFloat(posY), width: 100, height: 30)). Since it should not be call manually.

UI Text Field postioning Swift

I am a newbie to ios development and am fairly confused about how to position a UI Text field in swift. I am working with sprite kit and am creating a gaem that involves inputing a quadratic equation. I want to position my text fields along side my labels so it say y=_x^2+x+. When I run the following code, my text fields are in the top left corner! What should I do?
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.blueColor()
//create y = label
let yLabel = SKLabelNode(fontNamed:"Note-Worthy-Bold")
yLabel.text = "Y = ";
yLabel.fontColor = SKColor.redColor()
yLabel.fontSize = 30;
yLabel.position = CGPoint(x:CGRectGetMidX(self.frame)-300, y:CGRectGetMidY(self.frame)+100);
self.addChild(yLabel)
//create text fielld for coefiecent of x^2 variable
var xSqrCoefInput = UITextField(frame:CGRectMake(10, 10, 30, 10))
xSqrCoefInput.backgroundColor = UIColor.orangeColor()
self.view?.addSubview(xSqrCoefInput)
//create x^2 Label
let xSqrLabel = SKLabelNode(fontNamed:"Note-Worthy-Bold")
xSqrLabel.text = "x^2+";
xSqrLabel.fontColor = SKColor.redColor()
xSqrLabel.fontSize = 30;
xSqrLabel.position = CGPoint(x:CGRectGetMidX(self.frame)-150, y:CGRectGetMidY(self.frame)+100);
self.addChild(xSqrLabel)
//create text fielld for coefiecent of x variable
var xCoefInput = UITextField(frame:CGRectMake(10, 10, 30, 10))
self.view?.addSubview(xCoefInput)
xCoefInput.backgroundColor = UIColor.orangeColor()
//create x label
let xLabel = SKLabelNode(fontNamed:"Note-Worthy-Bold")
xLabel.text = "x+";
xLabel.fontColor = SKColor.redColor()
xLabel.fontSize = 30;
xLabel.position = CGPoint(x:CGRectGetMidX(self.frame)-0, y:CGRectGetMidY(self.frame)+100);
self.addChild(xLabel)
//create text fielld for constant
var constantInput = UITextField(frame:CGRectMake(10, 10, 30, 10))
self.view?.addSubview(constantInput)
constantInput.backgroundColor = UIColor.orangeColor()
}
CGRectMake takes in 4 parameters.
CGRectMake(x position, y position, width, height)
The default starting position is the top-left corner. Your current textfield position is (10,10), so basically you just have to change the x and y positions for all the textfields.
var xSqrCoefInput = UITextField(frame:CGRectMake(10, 10, 30, 10))
If you wish to place the textfield next to the labels, it would be best to get the position of the labels and set the position from there.

Resources