How To Expand UITableViewCell's UILabel's Background Property When Text Is Being Generated?

112 Views Asked by At

I cannot get my label's background property to properly expand/grow with the text as it is being inputted into the label.

I've made a UITableView with a custom cell. This custom cell's file is a .xib file (called StoryCell.xib). The .xib file is associated with a .swift file that I can enter code in. In this .swift file (it is called StoryCell.swift), I have a typing animation function where whatever string goes into this label, it will be typed out character by character until the full text of that string variable is fully typed out over a period of time. With this label, I have a background property set. (for effects).

My problem is that when the characters and words are being typed into the label with the inputted string variable, the background property does not properly expand with each line of the text being typed out. Playing around with different code and enabling different properties and constraints, my label's background will either be pre-determined (meaning that the background property will automatically be set to certain dimensions [a certain box] to fit the entirety of the text once it's fully typed out) or that my background property just stays as one single line, cutting off text after that line moves onto another line to write more text.

I cannot for the life of me figure out what to do. If anyone has any idea on what I can do or resources I can look into, I'll be eternally grateful.

TL;DR I want my UILabel's background property in my custom cell (in a UITableView) to expand line by line with my text being typed out by my typing function, but I can't achieve this effect. How to get?

My StoryCell.swift's code for reference if you need:

import UIKit

class StoryCell: UITableViewCell {
    
    var typingTimer: Timer? // Add typingTimer property

    @IBOutlet weak var label: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // Configure the view for the selected state
    }

    // Add startTypingAnimation method
    func startTypingAnimation(withText text: String) {
        label.text = "" // Clear existing text
        var charIndex = 0

        // Create a timer to simulate typing animation
        typingTimer?.invalidate() // this code line here is to make sure that no previous timers are running
        
        typingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
            if charIndex < text.count {
                let index = text.index(text.startIndex, offsetBy: charIndex) // The text.index(_, offsetBy:) method is used to obtain the index of a character in the string text. It takes two arguments: the starting index and an offset. The offset specifies the number of positions to move from the starting index.

                let nextChar = String(text[index]) // the character at the position specified by the offset value
                
                self.label.text?.append(nextChar)
                charIndex += 1
            } else {
                timer.invalidate()
            }
        }
    }
}

If you must know, the label's text comes from another .swift file which is why you won't see any text in this file.

2

There are 2 best solutions below

5
Sweeper On

I'm not sure why exactly your constraints didn't work. I remember doing something similar before, and the cell automatically resizes as expected.

In any case, the cell does automatically resize if you use the UIListContentConfiguration API, which existed since iOS 14. This allows you customise table view cells in various ways, without manually adding the subviews yourself. Of course, it is less flexible than hand-crafting your own cells, but the customisation it allows is still quite a lot. There are also related classes like UIBackgroundConfiguration.

Here is startTypingAnimation, but changed to use UIListContentConfiguration:

// in awakeFromNib, set an initial configuration:
self.contentConfiguration = self.defaultContentConfiguration()

// ...

func startTypingAnimation(withText text: String) {
    var config = self.contentConfiguration as! UIListContentConfiguration
    config.text = ""
    self.contentConfiguration = config
    //label.text = ""
    var charIndex = 0

    typingTimer?.invalidate()
    
    typingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
        if charIndex < text.count {
            let index = text.index(text.startIndex, offsetBy: charIndex)

            let nextChar = String(text[index]) // the character at the position specified by the offset value
            
            var newConfig = self.contentConfiguration as? UIListContentConfiguration
            newConfig?.text?.append(nextChar)
            self.contentConfiguration = newConfig
            //self.label.text?.append(nextChar)
            charIndex += 1
        } else {
            timer.invalidate()
        }
    }
}
1
DonMag On

When a table view lays out its rows/cells...

  • is heightForRowAt indexPath implemented?
    • use that value
  • else, is tableView.rowHeight set to a specific value?
    • use that value
  • else, ask the cell for its height (based on its constraints and data)
    • use that value

Once the table is displayed, it no longer modifies its row heights.

So, if your code changes the content of the cell in a way that its height will change, we have to inform the controller and let it re-layout the cell(s).

This is commonly done with a closure in the cell class.

So, assuming your cell xib looks something like this:

enter image description here

We can add this to your cell class:

// closure to inform the table view controller that the text has changed
var textChanged: (() -> ())?

then, when your timer adds a new character to the label's text:

self.label.text?.append(nextChar)
charIndex += 1
                
// inform the controller that the text has changed
self.textChanged?()
                

Back in your controller class, in cellForRowAt, we set the closure:

cell.textChanged = { [weak self] in
    guard let self = self else { return }
        
    // this will update the row height if needed
    self.tableView.performBatchUpdates(nil)
}

Here is a complete example (assuming you have StoryCell.xib as shown above)...

Cell Class

class StoryCell: UITableViewCell {
    
    // closure to inform the table view controller that the text has changed
    var textChanged: (() -> ())?
    
    var typingTimer: Timer? // Add typingTimer property
    
    @IBOutlet weak var label: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // Configure the view for the selected state
    }
    
    // Add startTypingAnimation method
    func startTypingAnimation(withText text: String) {
        label.text = "" // Clear existing text
        var charIndex = 0
        
        // Create a timer to simulate typing animation
        typingTimer?.invalidate() // this code line here is to make sure that no previous timers are running
        
        typingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] timer in
            guard let self = self else {
                timer.invalidate()
                return
            }
            if charIndex < text.count {
                let index = text.index(text.startIndex, offsetBy: charIndex) // The text.index(_, offsetBy:) method is used to obtain the index of a character in the string text. It takes two arguments: the starting index and an offset. The offset specifies the number of positions to move from the starting index.
                
                let nextChar = String(text[index]) // the character at the position specified by the offset value
                
                self.label.text?.append(nextChar)
                charIndex += 1
                
                // inform the controller that the text has changed
                self.textChanged?()
                
            } else {
                timer.invalidate()
            }
        }
    }
}

a simple struct for the cell text

struct StoryStruct {
    var prompt: String = ""
    var story: String = ""
    var isStoryShowing: Bool = false
}

and an example view controller - (assign a new, plain view controller to StoryVC):

class StoryVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    let tableView = UITableView()
    
    var myData: [StoryStruct] = [
        StoryStruct(prompt: "What's going on?", story: "Learn App Development!"),
        StoryStruct(prompt: "What is a UILabel?", story: "UILabel\n\nA label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label."),
        StoryStruct(prompt: "What is a UIButton?", story: "UIButton\n\nA button displays a plain styled button that can have a title, subtitle, image, and other appearance properties."),
        StoryStruct(prompt: "What is a UISwitch?", story: "UISwitch\n\nA switch displays an element that shows the user the boolean state of a given value.\n\nBy tapping the control, the state can be toggled."),
        StoryStruct(prompt: "What next?", story: "Congratulations! You've now learned everything there is to know about developing iPhone Apps!"),
    ]
    
    var activeRow: Int = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: g.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        tableView.register(UINib(nibName: "StoryCell", bundle: nil), forCellReuseIdentifier: "StoryCell")
        tableView.dataSource = self
        tableView.delegate = self
        
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "StoryCell", for: indexPath) as! StoryCell
        
        cell.selectionStyle = .none
        
        let ss: StoryStruct = myData[indexPath.row]
        
        if ss.isStoryShowing {
            cell.label.text = ss.story
        } else {
            cell.label.text = ss.prompt
        }
            
        cell.textChanged = { [weak self] in
            guard let self = self else { return }
            
            // this will update the row height if needed
            self.tableView.performBatchUpdates(nil)
        }
            
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if indexPath.row == activeRow {
            if let c = tableView.cellForRow(at: indexPath) as? StoryCell {
                c.startTypingAnimation(withText: myData[indexPath.row].story)
                myData[indexPath.row].isStoryShowing = true
            }
            activeRow += 1
        }
    }
}

When run, it will look like this:

enter image description here

As you tap each row from top-to-bottom, your typing simulation will change the text in the row and it will expand:

enter image description here

Note - you need to set the Content Mode on the label to Top Left:

enter image description here

otherwise, the text in the label will "bounce" as it is resized.

Keep in mind -- this is Example Code Only!!! to get you on your way.