Search code examples
iosswiftobjective-cxcodeuitextview

Slow load time with UITextView as compared to UITextField


I am working on a project that creates a form dynamically based on an XML file. Everything works fine, except that if a form has too many textViews, it loads very slowly. Analyzing the app performance through xcode Time profiler, I came to know that whenever a textView is initailized it consumes a lot of CPU.

To make this easier to understand, I created a sample project that recreates this issue. The sample project just adds about 1000 UITextViews when a ViewController is loaded. I checked the performance of this code using Xcode Profiler. I was able to figure out that if UITextView is replaced by UITextField, the performance increases dramatically. The demo project would show a slight improvement since the view is limited with just textviews or textfields. But in my actual app, I see an improvement of 4 seconds minimum when I try to load a heavy XML.

Sample Project: https://github.com/amrit42087/TextView_Performance

Here is the screenshot of the Xcode Profiler with UITextView:

enter image description here

Here is the screenshot of the Xcode Profiler with UITextField:

enter image description here

UITextField reduces the load time to 960 ms from 1.49 seconds which is in case of UITextView. My actual project notices quite a lot of improvement since that is a heavy application.

Question: If anyone could suggest a solution to improve the performance while using a UITextView, that would be greatly appreciated. Using UITextField is not an option since I need multiline text.


Solution

  • Since we have no idea what else you might be doing...

    One thing to try is to use a UIView subclass that contains a label and a text view. Only show the text view after tapping on the label.

    Try replacing your SecondViewController with this:

    class SecondViewController: UIViewController {
    
        let scrollView = UIScrollView()
        let stackView = UIStackView()
        
        var myData: [String] = []
        
        let spinner = UIActivityIndicatorView()
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .yellow
        
            scrollView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            scrollView.keyboardDismissMode = .onDrag
    
            stackView.axis = .vertical
            stackView.spacing = 20
            
            stackView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(stackView)
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(scrollView)
            
            let g = view.safeAreaLayoutGuide
            let cg = scrollView.contentLayoutGuide
            let fg = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
    
                stackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
                stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
                stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
                stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
                
                stackView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
    
            ])
    
            spinner.style = .large
            spinner.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(spinner)
            spinner.hidesWhenStopped = true
            spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
            spinner.startAnimating()
            
            myData = (1...1000).compactMap({ "Element \($0)" })
    
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
                self.addTheViews()
            })
        }
    
        func addTheViews() {
            let start = CFAbsoluteTimeGetCurrent()
            
            let n: Int = 1000
            for i in 0..<n {
                addTextView(i)
            }
            
            let diff = CFAbsoluteTimeGetCurrent() - start
            print("Took \(diff) seconds")
            
            spinner.stopAnimating()
        }
        
        private func addTextView(_ idx: Int) {
            let textView = MyTextView()
            textView.myID = idx
            textView.text = myData[idx]
            textView.backgroundColor = .white
            textView.heightAnchor.constraint(equalToConstant: 120.0).isActive = true
            stackView.addArrangedSubview(textView)
        }
        
    }
    

    and an example MyTextView class:

    class MyTextView: UIView, UITextViewDelegate {
        
        public var myID: Int = 0
        
        public var text: String = "" {
            didSet { label.text = text }
        }
        
        public var textChanged: ((Int, String) -> ())?
        
        private let label = UILabel()
        private var textView: UITextView!
        private var tap: UITapGestureRecognizer!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            label.numberOfLines = 0
            label.translatesAutoresizingMaskIntoConstraints = false
            addSubview(label)
            let g = self
            NSLayoutConstraint.activate([
    
                label.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
                label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 5.0),
                label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -5.0),
                label.bottomAnchor.constraint(lessThanOrEqualTo: g.bottomAnchor, constant: 0.0),
                
            ])
            
            tap = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
            addGestureRecognizer(tap)
            
        }
        @objc func gotTap(_ g: UITapGestureRecognizer) {
            if textView == nil {
                textView = UITextView()
                if let f = label.font {
                    textView.font = f
                }
            }
            textView.text = self.text
            textView.frame = self.bounds
            textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
            addSubview(textView)
            
            textView.delegate = self
            
            textView.becomeFirstResponder()
        }
        func textViewDidChange(_ textView: UITextView) {
            textChanged?(myID, textView.text ?? "")
        }
        func textViewDidBeginEditing(_ textView: UITextView) {
            removeGestureRecognizer(tap)
        }
        func textViewDidEndEditing(_ textView: UITextView) {
            textView.removeFromSuperview()
            addGestureRecognizer(tap)
        }
        
    }
    

    This is, of course, just a "starting point" but my be helpful.