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:
Here is the screenshot of the Xcode Profiler with UITextField:
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.
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.