Adjust text size to fit SKLabelNode with fixed width - sprite-kit

New to Spritekit and trying to fix the width of a SKLabelNode and then adjust the font of text to fit within the node accordingly.
Have been looking through the docs but cant seem to find anything suitable, like the UILabel function:
A UILabel.adjustsFontSizeToFitWidth = true
Thanks

This is a nice function that I found a while back, but I can't remember exactly where
func adjustLabelFontSizeToFitRect(labelNode:SKLabelNode, rect:CGRect) {
// Determine the font scaling factor that should let the label text fit in the given rectangle.
let scalingFactor = min(rect.width / labelNode.frame.width, rect.height / labelNode.frame.height)
// Change the fontSize.
labelNode.fontSize *= scalingFactor
// Optionally move the SKLabelNode to the center of the rectangle.
labelNode.position = CGPoint(x: rect.midX, y: rect.midY - labelNode.frame.height / 2.0)
}
This adjusts the font size of the label to fit the width exactly, but you may want to change the function in order to add some extra padding on all sides.

Related

How do you get the aspect fit size of a uiimage in a uimageview?

When print(uiimage.size) is called it only gives the width and the height of the original image before it was scaled up or down. Is there anyway to get the dimensions of the aspect fitted image?
Actually, there is a function in AVFoundation that can calculate this for you:
import AVFoundation
let fitRect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
now fitRect.size is the size inside the imageView bounds by maintaining the original aspect ratio.
You're going to need to calculate the resulting image size in Points yourself*.
* It turns out you don't. See Alladinian's answer. I'm going to
leave this answer here to explain what the library function is doing.
Here's the math:
let imageAspectRatio = image.size.width / image.size.height
let viewAspectRatio = imageView.frame.width / imageView.frame.height
var fitWidth: CGFloat // scaled width in points
var fitHeight: CGFloat // scaled height in points
var offsetX: CGFloat // horizontal gap between image and frame
var offsetY: CGFloat // vertical gap between image and frame
if imageAspectRatio <= viewAspectRatio {
// Image is narrower than view so with aspectFit, it will touch
// the top and bottom of the view, but not the sides
fitHeight = imageView.frame.height
fitWidth = fitHeight * imageAspectRatio
offsetY = 0
offsetX = (imageView.frame.width - fitWidth) / 2
} else {
// Image is wider than view so with aspectFit, it will touch
// the sides of the view but not the top and bottom
fitWidth = imageView.frame.width
fitHeight = fitWidth / imageAspectRatio
offsetX = 0
offsetY = (imageView.frame.height - fitHeight) / 2
}
Explanation:
It helps to draw the pictures. Draw a rectangle that represents the
imageView. Then draw a rectangle that is narrow but extends from the
top of the image view to the bottom. That is the first case. Then draw
one where the image is short but extends to the two side of the image
view. That is the second case. At that point, we know one of the
dimensions. The other is just that value multiplied or divided by the
image's aspect ratio because we know that the .aspectFit keeps the
image's original aspect ratio.
A note about frame vs. bounds. The frame is in the coordinate system of the view's superview. The bounds are in the coordinate system of the view itself. I chose to use the frame in this example, because the OP was interested in how far to move the imageView in it's superview's coordinates. For a standard imageView that has not been rotated or scaled further, the width and height of the frame will match the width and height of the bounds. Things get interesting though when a rotation is applied to an imageView. The frame expands to show the whole imageView, but the bounds remain the same.

Swift 5: centerXAnchor won't center my frame in the view

I'm stuck at the Consolidation IV challenge from hackingwithswift.com.
Right now I'm trying to create a hangman game. I thought to place placeholder labels based on the length of the answer word. These placeholder labels would be placed inside a frame, which then would be placed in the center of the main view.
Unfortunately, the leading edge of the frame is placed centered. In my opinion, this is not a problem of constraints, but rather a problem of me creating the frame wrong.
My current code is
import UIKit
class ViewController: UIViewController {
var answer: String!
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - declare all the labels here
let letterView = UIView()
letterView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(letterView)
// MARK: - set constraints to all labels, buttons etc.
NSLayoutConstraint.activate([
letterView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
letterView.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor)
])
// MARK: - populate the letterView
// set the size of the placeholder
answer = "Atmosphäre"
let height = 60
let width = 25
// var width: Int
for placeholder in 0..<answer.count {
// create new label and give it a big font size
let placeholderLabel = UILabel()
placeholderLabel.text = "_"
placeholderLabel.font = UIFont.systemFont(ofSize: 36)
// calculate frame of this label
let frame = CGRect(x: placeholder * width, y: height, width: width, height: height)
placeholderLabel.frame = frame
// add label to the label view
letterView.addSubview(placeholderLabel)
}
}
}
The simulator screen looks just like this:
I already searched for answers on stackoverflow, but wasn't successful. I think I don't know what I'm exactly looking for.
The main problem, is that the letterView has no size, because no width or height constraints are applied to it.
To fix your code make the letterView big enough to contain the labels you've added as subviews by adding height and width constraints after the for loop:
for placeholder in 0..<answer.count {
...
}
NSLayoutConstraint.activate([
letterView.widthAnchor.constraint(equalToConstant: CGFloat(width * answer.count)),
letterView.heightAnchor.constraint(equalToConstant: CGFloat(height))
])
I'm not sure if you've covered this in your course yet, but a better way to go about this (which would take much less code), is to use a UIStackView as your letterView instead.
An extra thing to consider:
If you give the letterView a background color, you'll see that the labels are actually aligned outside of its bounds:
That's because you're setting each label's y position to be height, when it should probably be zero:
let frame = CGRect(x: placeholder * width, y: 0, width: width, height: height)
Correcting this places the labels within the bounds of the letterView:

How to draw a line in a custom view (Retina) with a constant intensity?

I want to write a custom drawing view, which should take advantage of the Retina display. I am using a 2019 MacBook Pro. To test the drawing I am just drawing parallel lines with a width of 1px. But the lines show an uneven brightness across the view.
I already tried the conversion from screen coordinates as posted in
https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/APIs/APIs.html#//apple_ref/doc/uid/TP40012302-CH5-SW2
This is the relevant code block from the views draw(...) method
// fill with a black background
let background: NSBezierPath = NSBezierPath(rect: bounds)
NSColor.black.setFill()
background.fill()
// create a path that displays vertical lines 1px wide
let path: NSBezierPath = NSBezierPath()
let height: CGFloat = self.bounds.height
var xPosition: CGFloat = 0.0
while self.frame.width > xPosition {
let rect:NSRect = NSRect(x: xPosition, y: 0, width: 0.5, height: height)
path.appendRect(rect)
xPosition += 10.0
}
NSColor.yellow.setFill()
path.fill()
The result looks like this picture
the lines getting lighter und darker again, they should have all the same intensity
Okay, I figured out the solution. I was not aware, that an "unscaled" MacBook Pro Monitor that's set to default/unscaled is actually scaled. Apple upscales the screen resolution by the factor 1,125 as default. To see the correct screen you need to choose scaled and get one step down to see the nativ screen resolution. I am still wondering if there are optimisation measures possible to get a better result even on the scaled display other than using wider lines

SKLabelNode Not Showing [SpriteKit]

I created a SKShapeNode called smallScreen and an SKLabelNode with the name screenLabel.
I later attempted to display screenLabel in smallScreen. When I ran the code however, it didn't work. I tried adding screenLabel as a child to self, and that seemed to allow the text to display. Whenever I added screenLabel as a child node to smallScreen, it wouldn't display. Please help, much thanks.
self.smallScreen = (self.childNodeWithName("screen") as? SKShapeNode)!
self.smallScreen.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height * 2/3)
screenLabel.text = "Screen"
screenLabel.fontSize = 30
screenLabel.fontColor = UIColor.redColor()
screenLabel.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height * 2/3)
screenLabel.hidden = false
self.smallScreen.addChild(screenLabel)
In Sprite-kit by default, a scene’s origin is placed in the lower-left corner of the view. Its default value is CGPointZero and you can’t change it. (source).
A sprite’s (SKSpriteNode) anchor point defaults to (0.5,0.5), which corresponds to the center of the frame (source).
SKShapeNode does not have an anchorPoint.
LearnCocos2D answered on this issue:
When you use an image you may need to align it on its node's position.
There's no other way to do so but through anchorPoint because the
image itself can't be modified (not easily anyway).
When you create a shape you have full control over the alignment of
the shape through CGPath. You can simply translate the path to change
the shape's alignment relative to the node's position.
This is my interpretation, not fact.
So , what happened to your code?
I don't know how you have initialized your shape, I make just an example:
/* Create a Shape Node representing a rect centered at the Node's origin. */
#available(iOS 8.0, *)
public convenience init(rectOfSize size: CGSize)
So we create the shape like this:
self.smallScreen = SKShapeNode.init(rectOfSize: CGSizeMake(200,200))
First, you place your SKShapeNode to the X center of the screen for the X coordinates, and Y = self.frame.size.height * 2/3. In fact, your smallScreen is visible.
Speaking about your label, you have add it to the smallScreen, so screenLabel must respect the alignment of his parent.
In fact you can see your label simply by making this change:
From:
screenLabel.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height * 2/3)
To:
screenLabel.position = CGPointZero
and your label is positioned to the center of the smallScreen according to the alignment of his parent.

How to set font size of SKLabelNode to fit in fixed size (Swift)

I have a square (200X200) with a SKLabelNode in it. The label shows score and it my be reach a large number. I want fit the number in the square.
How can i change text size (or Size) of a SKLabelNode to fit it in a fixed size.
The size of the frame of the SKLabelNode can be compared against the given rectangle. If you scale the font in proportion to the sizes of the label's rectangle and the desired rectangle, you can determine the best font size to fill the space as much as possible. The last line conveniently moves the label to the center of the rectangle. (It may look off-center if the text is only short characters like lowercase letters or punctuation.)
Swift
func adjustLabelFontSizeToFitRect(labelNode:SKLabelNode, rect:CGRect) {
// Determine the font scaling factor that should let the label text fit in the given rectangle.
let scalingFactor = min(rect.width / labelNode.frame.width, rect.height / labelNode.frame.height)
// Change the fontSize.
labelNode.fontSize *= scalingFactor
// Optionally move the SKLabelNode to the center of the rectangle.
labelNode.position = CGPoint(x: rect.midX, y: rect.midY - labelNode.frame.height / 2.0)
}
Objective-C
-(void)adjustLabelFontSizeToFitRect:(SKLabelNode*)labelNode rect:(CGRect)rect {
// Determine the font scaling factor that should let the label text fit in the given rectangle.
double scalingFactor = MIN(rect.size.width / labelNode.frame.size.width, rect.size.height / labelNode.frame.size.height);
// Change the fontSize.
labelNode.fontSize *= scalingFactor;
// Optionally move the SKLabelNode to the center of the rectangle.
labelNode.position = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect) - labelNode.frame.size.height / 2.0);
}
I have written this for the width, but you can adapt it to the height to fit your CGRect. In the example, pg is a SKLabelNode initialized with the font you are using. Arguments are your String and the target width, and the result is the size you want to assign to your SKLabelNode. Of course, you can also put directly your SKLabelNode. If the size is too big, then the max size is 50, but that's personal.
func getTextSizeFromWidth(s:String, w:CGFloat)->CGFloat {
var result:CGFloat = 0;
var fits:Bool = false
pg!.text=s
if(s != ""){
while (!fits) {
result++;
pg!.fontSize=result
fits = pg!.frame.size.width > w;
}
result -= 1.0
}else{
result=0;
}
return min(result, CGFloat(50))
}
Edit: Actually, I just realized I had also written this:
extension SKLabelNode {
func fitToWidth(maxWidth:CGFloat){
while frame.size.width >= maxWidth {
fontSize-=1.0
}
}
func fitToHeight(maxHeight:CGFloat){
while frame.size.height >= maxHeight {
fontSize-=1.0
}
This expansion of mike663's answer worked for me, and gets there much quicker than going 1 pixel at a time.
// Find the right size by trial & error...
var testingSize: CGFloat = 0 // start from here
var currentStep: CGFloat = 16 // go up by this much. It will be reduced each time we overshoot.
var foundMatch = false
while !foundMatch {
var overShot = false
while !overShot {
testingSize += currentStep
labelNode.fontSize = testingSize
// How much bigger the text should be to perfectly fit in the given rectangle.
let scalingFactor = min(rect.width / labelNode.frame.width, rect.height / labelNode.frame.height)
if scalingFactor < 1 { overShot = true } // Never go over the space
else if scalingFactor < 1.01 { foundMatch = true } // Stop when over 99% of the space is filled
}
testingSize -= currentStep // go back to the last one that didn't overshoot
currentStep /= 4
}
labelNode.fontSize = testingSize // go back to the one we were happy with