How to display UILabel one line at a time in Swift - ios

I have a UILabel in a tableView cell that I would like to animate line by line. The string has line breaks with \n and lines.count returns 7 as expected but no line is being added to the label at all even though the string itself is not nil.
Code in cellForRow
var index = 0
let lines = messageList[indexPath.row].message.components(separatedBy: "\n")
senderCell.otherUserMessage.numberOfLines = lines.count
Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { [weak self] timer in
if index != lines.count {
let char = lines[index]
let nChar = "\n\(char)"
senderCell.otherUserMessage.text! += nChar
print("char is \(nChar)") //nChar prints the proper text
print("senderCellText is \(senderCell.otherUserMessage.text!)") // but this prints an array of numbers
// Play sound each time
do {
self!.audioPlayer = try AVAudioPlayer(contentsOf: self!.koalaMessageSound)
self!.audioPlayer.play()
}catch{
// couldn't load file :(
}
index += 1
} else {
timer.invalidate()
}
})
Idea/Expected Result

Only way I found to animate a Label like this was to:
1. create a copy of Label for each line you have in your original Label (calculating frame.height / lineHeight)
2. mask each of those copies with rectangles for single line
3. animate each Label separately - each Label will be displaying one line only there so you can control them separately.
Note: Using \n is impossible, because sometimes text may go to next line due to it's length on smaller devices, so to get real count of lines, you need to calculate like like I described on point 1.

Related

How to word wrap on UILabel that has infinite lines?

I have a UILabel in which the number of lines need to be set to 0 (infinite lines). But when it displays a single long word, it breaks the line by character, to form a second line. I tried to set Line breaking mode manually as such
cell.nameLbl.adjustsFontSizeToFitWidth = true
cell.nameLbl.lineBreakMode = .byWordWrapping
But the app still breaks by character. How can I fix this?
Edit- I want the word font size to shrink.
Adjusting font size is unfortunately done per label, not per line. Wrapping does not effect it. So the result you are seeing is expected. You could split your string manually into multiple labels maybe putting them into stack view to get desired result.
What I tried was:
private func layoutText(text: String, onStackView stackView: UIStackView) {
var words: [String] = text.components(separatedBy: .whitespaces)
var currentLineWords: [String] = [String]()
var currentLabel: UILabel!
func newLabel() -> UILabel {
let label = UILabel(frame: stackView.bounds)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
return label
}
while words.count > 0 {
if currentLineWords.count == 0 {
currentLabel = newLabel()
currentLineWords.append(words.removeFirst())
} else {
let newText = currentLineWords.joined(separator: " ") + " " + words[0]
currentLabel.text = newText
currentLabel.sizeToFit()
if currentLabel.bounds.width > stackView.bounds.width {
// Break line
currentLabel.text = currentLineWords.joined(separator: " ")
stackView.addArrangedSubview(currentLabel)
currentLabel = nil
currentLineWords = [String]()
} else {
// All good
currentLineWords.append(words.removeFirst())
}
}
}
if currentLineWords.count > 0 {
currentLabel.text = currentLineWords.joined(separator: " ")
stackView.addArrangedSubview(currentLabel)
}
}
This works pretty neatly but:
When word is too long to fit in a line even when shrunk it will show "..." at the end
Shrunk line still has the same height instead of having it reduced
We are only handling whitespaces and inserting back spaces
The first 2 would probably both be solved by having manual adjustsFontSizeToFitWidth. Basically keep reducing font size until it fits or until a certain size. If "certain size" scenario is hit then simply use multiline on a label (will break by characters).
The last one just need some extra effort.
I would still not implement this for multiple reasons one of them being that it looks ugly but is still interesting.
Custom font size adjustment:
To have custom font adjustment and all the logic there is only a change when a new lien is created:
if currentLineWords.count == 0 {
currentLabel = newLabel()
currentLineWords.append(words.removeFirst())
currentLabel.text = currentLineWords.joined(separator: " ")
currentLabel.sizeToFit()
if currentLabel.bounds.width > stackView.bounds.width {
currentLabel.adjustsFontSizeToFitWidth = false
let originalFontSize: CGFloat = currentLabel.font.pointSize
var currentFontSize: CGFloat = originalFontSize
let minimalFontSize: CGFloat = currentLabel.minimumScaleFactor > 0.0 ? currentLabel.font.pointSize * currentLabel.minimumScaleFactor : originalFontSize
while currentLabel.bounds.width > stackView.bounds.width {
currentFontSize -= 1.0
if currentFontSize < minimalFontSize {
currentLabel.numberOfLines = 0
currentLabel.font = UIFont(name: currentLabel.font.fontName, size: originalFontSize)
break // Could not shrink it even further. USe multiline
}
currentLabel.font = UIFont(name: currentLabel.font.fontName, size: currentFontSize)
currentLabel.sizeToFit()
}
// Break line
stackView.addArrangedSubview(currentLabel)
currentLabel = nil
currentLineWords = [String]()
}
}
This looks usable now. My result:

iOS: Programmatically move cursor up, down, left and right in UITextView

I use the following code to move the cursor position to 5 characters from the beginning of a UITextField:
txtView.selectedRange = NSMakeRange(5, 0);
Now, if I have a cursor at an arbitrary position as shown in the image below, how can I move the cursor up, down, left and right?
Left and right should be more or less easy. I guess the tricky part is top and bottom. I would try the following.
You can use caretRect method to find the current "frame" of the cursor:
if let cursorPosition = answerTextView.selectedTextRange?.start {
let caretPositionRect = answerTextView.caretRect(for: cursorPosition)
}
Then to go up or down, I would use that frame to calculate estimated position in UITextView coordinates using characterRange(at:) (or maybe closestPosition(to:)), e.g. for up:
let yMiddle = caretPositionRect.origin.y + (caretPositionRect.height / 2)
let lineHeight = answerTextView.font?.lineHeight ?? caretPositionRect.height // default to caretPositionRect.height
// x does not change
let estimatedUpPoint = CGPoint(x: caretPositionRect.origin.x, y: yMiddle - lineHeight)
if let newSelection = answerTextView.characterRange(at: estimatedUpPoint) {
// we have a new Range, lets just make sure it is not a selection
newSelection.end = newSelection.start
// and set it
answerTextView.selectedTextRange = newSelection
} else {
// I guess this happens if we go outside of the textView
}
I haven't really done it before, so take this just as a general direction that should work for you.
Documentation to the methods used is here.

How to find and replace UILabel's value on a specific line?

I know I can find the total number of lines of a UILabel with .numberOfLines but how can I retrieve and edit the value say on line 2.
Example:
Assuming from your screen shot that each line is separated by a newline character you can split the text based on that.
Here is an example in Swift 3:
if let components = label.text?.components(separatedBy: "\n"), components.count > 1 {
let secondLine = components[2]
let editedSecondLine = secondLine + "edited"
label.text = label.text?.replacingOccurrences(of: secondLine, with: editedSecondLine)
}
You should make sure there is a value at whatever index your interested in. This example makes sure that there are more than a single component before retrieving the value.
You can then replace the second line with your edited line.
Hope that helps.

UILabels within an array not showing up on ViewController

So I'm trying to make a grid/table show up in my app, and to do this I created an array of UILabels with:
var rows = 2
var columns = 2
var grid = [[UILabel]](count: rows, repeatedValue: [UILabel](count: columns, repeatedValue: UILabel()))
then I created each UILabel with:
for i in 0 ..< grid.count
{
for j in 0 ..< grid[0].count
{
grid[i][j].frame = CGRectMake(x + CGFloat(j*Int(gridSpace)), y + CGFloat(i*Int(gridSpace)), gridSpace, gridSpace)
grid[i][j].backgroundColor = UIColor.redColor()
grid[i][j].hidden = false
grid[i][j].textColor = UIColor.whiteColor()
grid[i][j].textAlignment = NSTextAlignment.Center
grid[i][j].text = "label: [\(i)][\(j)]"
self.view.addSubview(grid[i][j])
print("grid[\(i)][\(j)]X = \(grid[i][j].frame.origin.x) grid[\(i)][\(j)]Y = \(grid[i][j].frame.origin.y)")
}
}
What happens is actually pretty interesting. The code compiles and everything, but only one of the labels shows up. The dimensions and everything about the label are perfect, but only one of them shows up.
The label that shows up is always the grid[rows-1][columns-1] label. But when I print the x and y coordinates of the rest of the labels that are supposed to be in the grid array, all of the coordinates are exactly where they are supposed to be on the viewcontroller it's just that the labels don't show up. when I changed the for loop to
for i in 0 ..< grid.count-1
{
for j in 0 ..< grid[0].count-1
{
still only the last label (the grid[rows-1][columns-1] label) shows up yet the coordinates are exactly where they should be. Another weird thing is that if I change the line
grid[i][j].text = "test label: [\(i)][\(j)]"
to
if(j != grid[0].count-1)
{
grid[i][j].text = "test label: [\(i)][\(j)]"
}
then the label that shows up still has the coordinates of the grid[rows-1][columns-1] but has the labeltext of grid[rows-1][columns-2]
Can anyone fix my problem for me or explain whats going on?
It's because of the way that you have initialized your grid with repeated values. If the repeated value is of reference type, the array will actually create only one item and point all indexes to that item, so in your for loops you're changing the frame for the one and only UILabel over and over again. thats why you only see the last one on your screen.
To create the array you want replace this:
var grid = [[UILabel]](count: rows, repeatedValue: [UILabel](count: columns, repeatedValue: UILabel()))
with
var grid = [[UILabel]]()
for row in 0..<rows {
grid.append([UILabel]())
for column in 0..<columns{
grid[row].append(UILabel())
}
}
or
var grid = [[UILabel]]((0..<rows).map { _ in
[UILabel]((0..<columns).map { _ in
UILabel()
})
})

Action not working correctly in SpriteKit

I'm new to iOS programing and I'm experimenting to learn trying to create a game in swift using Sprite Kit.
What I'm trying to achieve is having a constant flow of blocks being created and moving rightwards on the screen.
I start by creating a set which contains all the initial blocks, then an action "constant movement" is added to each one, which makes them move slowly to the right. What I'm having trouble is adding new blocks to the screen.
The last column of blocks has an "isLast" boolean set to true, when it passes a certain threshold it is supposed to switch to false and add a new column of blocks to the set which now have "isLast" set to true.
Each block in the set has the "constantMovement" action added which makes them move slowly to the right, the new blocks have it added as well, but they don't work as the original ones.
Not all of the move, even tho if I print "hasActions()" it says they do, and the ones that do move stop doing so when they get to the middle of the screen. I have no idea why this happens, can somebody experienced give me a hint please?
This is the update function:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let constantMovement = SKAction.moveByX(-1, y: 0, duration: 10);
background.runAction(SKAction.repeatActionForever(constantMovement));
let removeBlock = SKAction.removeFromParent();
let frame = self.frame;
var currentBlockSprite:SKSpriteNode;
var newBlock: Block;
for block in blocks {
currentBlockSprite = block.sprite!;
currentBlockSprite.runAction(constantMovement);
if(block.column == NumColumns - 1) {
block.isLast = true;
}
if(block.isNew) {
println("position \(currentBlockSprite.position.x) has actions \(currentBlockSprite.hasActions())");
}
if(block.isLast && currentBlockSprite.position.x < frame.maxX - 50) {
println("the block that hits is " + block.description);
println("HITS AT \(currentBlockSprite.position.x)");
block.isLast = false;
for row in 0..<NumRows {
newBlock = Block(column: NumColumns - 1, row: row, blockType: BlockType.random(), isLast: true, isNew: true);
blocks.addElement(newBlock);
addBlockSprite(newBlock);
println("new block: " + newBlock.description + "position \(newBlock.sprite?.position.x)");
}
}
if(currentBlockSprite.position.x < frame.minX) {
currentBlockSprite.runAction(removeBlock);
blocks.removeElement(block);
}
}
}
My whole project is in here: https://github.com/thanniaB/JumpingGame/tree/master/Experimenting
but keep in mind that since I'm new to this it might be full of cringeworthy bad practices.
I would remove any SKAction code from the update function as that's kind of a bad idea. Instead I would just apply the SKAction when you add your block sprite to the scene, like this.
func addBlockSprite(block: Block) {
let blockSprite = SKSpriteNode(imageNamed: "block");
blockSprite.position = pointForColumn(block.column, row:block.row);
if(block.blockType != BlockType.Empty) {
addChild(blockSprite);
let constantMovement = SKAction.moveByX(-10, y: 0, duration: 1)
var currentBlockSprite:SKSpriteNode
let checkPosition = SKAction.runBlock({ () -> Void in
if(blockSprite.position.x < -512){
blockSprite.removeAllActions()
blockSprite.removeFromParent()
}
})
let movementSequence = SKAction.sequence([constantMovement, checkPosition])
let constantlyCheckPosition = SKAction.repeatActionForever(movementSequence)
blockSprite.runAction(constantlyCheckPosition)
}
block.sprite = blockSprite;
}
That would then allow you to simply add a new block whenever you see fit and it will have the appropriate action when it's added.
I've used 512 as thats the size of the iPhone 5 screen but you could swap this out for another screen size or what would be better would be a variable that dynamically reflects the screen size.

Resources