I want to have a UILabel in the middle of the screen that starts from the left of the screen. The UILabel will get updated every second from a Timer to simulate a typewriter effect. So to display "Album releases", the UILabel will show this series of text:
A Al Alb Albu Album Album Album r Album re Album rel Album rele Album relea Album releas Album release Album releases
The label has a black background. So I want to make sure the width fits the text. But if the text is too long then the width should go up to the screen width and then breaks into a second line. I tried using numberOfLines = 0 and sizeToFit to achieve that. But the issue is that for some reason, one letter is being written on a line. I cannot manage to fix that. Here's some code:
private let taglinesLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.textColor = .white
label.backgroundColor = .black
label.numberOfLines = 0
return label
}()
override func layoutSubviews() {
...
taglinesLabel.frame = CGRect(x: 0, y: 200, width: 0, height: 0)
...
}
public func showText() {
...
startTimer()
...
}
func startTimer () {
guard timer == nil else { return }
timer = Timer.scheduledTimer(
timeInterval: TimeInterval(0.1),
target : self,
selector : #selector(onTimer),
userInfo : nil,
repeats : true)
}
@objc private func onTimer() {
let tagline = taglines[taglineIndex]
if(taglineCharacterIndex < tagline.count) {
let substring = tagline.prefix(taglineCharacterIndex)
print(substring)
taglinesLabel.text = String(substring)
taglinesLabel.sizeToFit()
taglineCharacterIndex += 1
}
}
How to constraint the UILabel to expand its width and not go to the next line like shown in the picture?
Couple things...
First, use auto-layout and constraints instead of .sizeToFit()
.
Second, let's replace the UILabel
with a non-editable, non-scrolling UITextView
because:
So, start with an example controller that puts an instance of our custom TypingView
40-points from the Top and from each side. We'll also give it four sample "taglines" and we'll start the timer in viewDidAppear
:
class ViewController: UIViewController {
let testView = TypingView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// no bottom or height constraint
// we let the TypingView set its own height
])
testView.taglines = [
"First sample string.",
"This is the Second sample tagline.",
"This tagline will be long enough that it will wrap onto at least two lines.",
"Here is the final tagline.",
]
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
testView.startTimer()
}
}
Now, our custom UIView
subclass:
class TypingView: UIView {
public var taglines: [String] = []
private var taglineIndex: Int = 0
private var timer: Timer!
private var taglineCharacterIndex: Int = 0
private let taglinesLabel: UITextView = {
let v = UITextView()
v.font = .systemFont(ofSize: 17.0, weight: .regular)
v.textColor = .white
v.backgroundColor = .black
v.isScrollEnabled = false
v.isEditable = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
backgroundColor = .black
taglinesLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(taglinesLabel)
NSLayoutConstraint.activate([
taglinesLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
taglinesLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
taglinesLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
taglinesLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
])
}
func startTimer () {
guard timer == nil else { return }
timer = Timer.scheduledTimer(
timeInterval: TimeInterval(0.1),
target : self,
selector : #selector(onTimer),
userInfo : nil,
repeats : true)
}
@objc private func onTimer() {
guard taglineIndex < taglines.count else {
timer.invalidate()
timer = nil
return
}
let tagline = taglines[taglineIndex]
if taglineCharacterIndex < tagline.count + 1 {
let substring = tagline.prefix(taglineCharacterIndex)
taglinesLabel.text = String(substring)
taglineCharacterIndex += 1
} else if taglineCharacterIndex < tagline.count + 6 {
// this will provide a half-second "pause" before going to the next tagline
taglineCharacterIndex += 1
} else {
taglineIndex += 1
taglineCharacterIndex = 0
}
}
}
It will look like this (adding 1 character every 1/10th second):
and with enough text to wrap:
Edit - in response to comment...
To get the view to expand horizontally as the text gets longer, change the trailing
constraint (in ViewController
) from:
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
to:
testView.trailingAnchor.constraint(lessThanOrEqualTo: g.trailingAnchor, constant: -40.0),
Now it will grow wider with each character, but only until it reaches 40-points from the right side.
You could also replace the trailing
constraint with a width
constraint if that would better suit your needs.
For example:
testView.widthAnchor.constraint(lessThanOrEqualToConstant: 240.0),
would limit its width to 240-points.