0

I have a UITableView with cells that display chat messages. Some messages contain code blocks which I render inside the cell by dynamically creating and adding custom CodeBlockView instances to a stack view within the cell. Here is an excerpt from my tableView(_:cellForRowAt:) method:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let row = indexPath.row
        
        let message = message(at: indexPath)
        if message.role == Role.assistant.rawValue {
            let chatCell = tableView.dequeueReusableCell(withIdentifier: ChatCell.identifier, for: indexPath) as! ChatCell
            chatCell.formattingStackView.removeAllArrangedSubviews()
            chatCell.index = row
            chatCell.delegate = self
            chatCell.text = message.content
            if TentativeChatModel.shared.tentativeMessage.content != "" && row == TentativeChatModel.shared.chatList.count - 1 && message.role == Role.assistant.rawValue {
                return chatCell
            }
            
            chatCell.configure(text: message.content)
            
            if let reasoning = message.reasoningContent, let time = message.think_time, time >= 0 {
                chatCell.collapseButton.isHidden = false
                chatCell.reasoningView.isHidden = false
                chatCell.thinkLabel.text = reasoning
                chatCell.thoughtForLabel.text = "Thought for \(time)s"
                if AppConstants.expandedRowIndex.contains(row) {
                    chatCell.reasoningView.borderWidth = 0.8
                    chatCell.thinkFooterView.isHidden = false
                    chatCell.chevronImageView.image = .chevronUp
                } else {
                    chatCell.reasoningView.borderWidth = 0
                    chatCell.thinkFooterView.isHidden = true
                    chatCell.chevronImageView.image = .chevronUp
                }
            } else {
                chatCell.reasoningView.isHidden = true
                chatCell.collapseButton.isHidden = true
            }
            
            let document = Document(parsing: message.content)
            let results = markdown.parserResults(from: document)
            for result in results {
                if result.isCodeBlock {
                    let spacer = UIView()
                    spacer.heightAnchor.constraint(equalToConstant: 10).isActive = true
                    
                    let codeCell = try? CodeBlockView.view(with: self) as? CodeBlockView
                    codeCell?.translatesAutoresizingMaskIntoConstraints = false
                    codeCell?.codeLabel.attributedText = result.attributedString.trimmedTrailingNewlines()
                    codeCell?.languageLabel.text = result.codeBlockLanguage ?? "Output"
                    
                    chatCell.formattingStackView.addArrangedSubview(codeCell!)
                    chatCell.formattingStackView.addArrangedSubview(spacer)
                } else {

                    let label = UILabel()
                    label.attributedText = result.attributedString
                    
                    label.textColor = .cLabel
                    label.translatesAutoresizingMaskIntoConstraints = false
                    label.numberOfLines = 0
                    label.lineBreakMode = .byWordWrapping
                    chatCell.formattingStackView.addArrangedSubview(label)
                }
            }
            
            if let citations = message.citations, !citations.isEmpty {
                let view = UIView()
                view.translatesAutoresizingMaskIntoConstraints = false
                view.heightAnchor.constraint(equalToConstant: 10).isActive = true
                chatCell.formattingStackView.addArrangedSubview(view)
                
                let collectionView = CitationsCollectionView(frame: .zero, collectionViewLayout: layoutForCitationsCollectionView())
                collectionView.configure(citations)
                collectionView.translatesAutoresizingMaskIntoConstraints = false
                collectionView.heightAnchor.constraint(equalToConstant: 180).isActive = true
                collectionView.widthAnchor.constraint(equalToConstant: tableView.frame.width).isActive = true
                chatCell.formattingStackView.addArrangedSubview(collectionView)
                
                collectionView.onCitationTap = { [weak self] title, citation in
                    guard let self else { return }
                    customDelegate?.chatTableView(self, didTapCitation: citation, titleForCitation: title)
                }
            }
            
            chatCell.streamLabel.isHidden = true
            chatCell.formattingStackView.isHidden = false
            return chatCell
        } else {
            let userCell = tableView.dequeueReusableCell(withIdentifier: UserCell.identifier, for: indexPath) as! UserCell
            userCell.configure(message: message.content)
            return userCell
        }
    }

View Hierarchy of my TableViewCell enter image description here

The problem is that when a cell contains one or more CodeBlockViews, scrolling the table view becomes very laggy and sometimes gets stuck.

What I suspect:

  • Creating and configuring views in cellForRowAt is heavy and blocking the main thread.

  • The markdown parsing and attributed string generation might also be expensive.

  • Lack of view reuse for the code block views inside cells. What I want to achieve:

  • Smooth scrolling performance even when multiple code blocks appear inside cells.

  • Efficient reuse or caching of views to avoid expensive view creation on every scroll.

Could anyone suggest best practices or solutions for efficiently handling dynamic subviews like custom code block views inside table view cells?

What I have tried:

  • Caching whole tableview cell.

2 Answers 2

0

Make reasonable use of the reuse mechanism. Use one cell for one message type. Do not dynamically create too many elements in the cell. This will avoid lags. For example, text messages, picture messages, and attachment messages should use their own cells and reuse identifiers.

Sign up to request clarification or add additional context in comments.

3 Comments

chatCell.formattingStackView.removeAllArrangedSubviews() avoid removeSubView and addSubView too many times just once
Do not add more details in comments. Edit your answer to include all information.
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
0

You need to optimize your code to do the minimum amount of processing in your cellForRowAt method.

I would suggest doing all your markdown processing before displaying the table view. Build some sort of in-memory model that represents what you are going to display so you can just create views and serve them up.

If that doesn't help, try running a time profile in instruments and figuring out where the time-consuming work is taking place. You might need to pre-build table view cells with commonly used view structures and set them up with specific identifiers.

If you are only serving up a couple of hundred cells (or less) you might also be able to get away with building an array of table view cells and just returning the pre-built cells in cellForRowAt. iPhones have a whole lot more memory than they did when UITableView was designed.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.