This is the code that I am using to acieve this:
import UIKit
class ViewController: UIViewController, UISearchBarDelegate {
var placeholderIndex = 0
var placeholderTexts = ["Text 1", "Text 2", "Text 3"]
var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
searchBar = UISearchBar()
searchBar.placeholder = "Search...." // Set initial placeholder
searchBar.searchBarStyle = .prominent
searchBar.sizeToFit()
searchBar.isTranslucent = true
searchBar.delegate = self
searchBar.backgroundImage = UIImage()
searchBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(searchBar)
Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(updatePlaceholder), userInfo: nil, repeats: true)
NSLayoutConstraint.activate([
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
searchBar.heightAnchor.constraint(equalToConstant: 70)
])
}
@objc func updatePlaceholder() {
let nextPlaceholderLabel = UILabel(frame: CGRect(x: 0, y: -searchBar.frame.height, width: searchBar.frame.width, height: searchBar.frame.height))
nextPlaceholderLabel.text = placeholderTexts[placeholderIndex]
nextPlaceholderLabel.textAlignment = .center
nextPlaceholderLabel.font = searchBar.searchTextField.font
nextPlaceholderLabel.textColor = searchBar.searchTextField.textColor
searchBar.addSubview(nextPlaceholderLabel)
UIView.animate(withDuration: 0.5, animations: {
nextPlaceholderLabel.frame.origin.y = 0
nextPlaceholderLabel.alpha = 1.0
self.searchBar.layoutIfNeeded()
}) { _ in
self.searchBar.placeholder = self.placeholderTexts[self.placeholderIndex]
nextPlaceholderLabel.removeFromSuperview()
}
placeholderIndex = (placeholderIndex + 1) % placeholderTexts.count
}
}
The function updatePlaceholder() is supposed to achieve this animation, however my result is coming something like this:
What changes do I need to do here? Thanks in advance
There are various ways to approach this... here's one...
The effect you want to replicate is animating individual words. When the placeholder text is changed, the current text animates down and fades out, word-by-word, and then the new text animates down (from above) and fades in, again word-by-word.
So, when we update the placeholder text we can:
So we can see what's going on, I've outlined the labels in red, and they are not yet "fading-out."
Again, so we can see what's going on, I've outlined these labels in blue, and visible on start. (They will eventually start with alpha-zero and then "fade-in.")
The final step is to update the .placeholder
text property, and remove all of the animated labels.
Here is a quick example:
class ViewController: UIViewController, UISearchBarDelegate {
// start index at -1 so we get the first item -- [0] -- with the first increment
var placeholderIndex = -1
let placeholderTexts = [
"What would you like to do?",
"Check your Balance",
"Send money to Mom",
"Text 1",
"Text 2",
"Text 3",
]
var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
searchBar = UISearchBar()
searchBar.placeholder = "Search...." // Set initial placeholder
searchBar.searchBarStyle = .prominent
searchBar.isTranslucent = true
searchBar.delegate = self
searchBar.backgroundImage = UIImage()
searchBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(searchBar)
NSLayoutConstraint.activate([
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
searchBar.heightAnchor.constraint(equalToConstant: 70)
])
Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(updatePlaceholder), userInfo: nil, repeats: true)
}
@objc func updatePlaceholder() {
// use these values for the placeholder label frame origin
// if we can't get find it via code
var baseX: CGFloat = 38.0
var baseY: CGFloat = 25.0
// try to find the placeholder label
// Apple did not make it public, so this is one approach
// note that if Apple changes the structure in the future, this may fail
var placeholderLabelFrame: CGRect = .zero
let tf: UITextField = searchBar.searchTextField
for subview in tf.subviews {
// Check if the subview is a UILabel and has the same text as the placeholder
if let label = subview as? UILabel, label.text == tf.placeholder {
placeholderLabelFrame = label.frame
}
}
if placeholderLabelFrame != .zero {
let cvtFrame = searchBar.searchTextField.convert(placeholderLabelFrame, to: searchBar)
baseX = cvtFrame.origin.x
baseY = cvtFrame.origin.y
}
// used to offset the labels vertically
var yOffset: CGFloat = 0.0
let hideIncrement: CGFloat = 2.0
let showIncrement: CGFloat = 8.0
// for positioning the labels
var currentX: CGFloat = 0
var toHideWords: [String] = []
var toShowWords: [String] = []
var toHideLabels: [UILabel] = []
var toShowLabels: [UILabel] = []
// array of Y-positions for the toHide labels to animate to
var targetY: [CGFloat] = []
let sbFont = searchBar.searchTextField.font
// split the current placeholder text into words
let hidePlaceholder: String = searchBar.placeholder ?? ""
toHideWords = hidePlaceholder.components(separatedBy: " ")
currentX = baseX
// create and position labels for each word
for word in toHideWords {
let wordLabel = UILabel()
wordLabel.text = word + " "
wordLabel.font = sbFont
wordLabel.textColor = .gray
wordLabel.sizeToFit()
wordLabel.frame.origin = .init(x: currentX, y: baseY)
currentX += wordLabel.frame.width
toHideLabels.append(wordLabel)
searchBar.addSubview(wordLabel)
}
// increment index into placeholderTexts
placeholderIndex = (placeholderIndex + 1) % placeholderTexts.count
// split the new placeholder text into words
let showPlaceholder: String = placeholderTexts[placeholderIndex]
toShowWords = showPlaceholder.components(separatedBy: " ")
currentX = baseX
// create and position labels for each word
for word in toShowWords {
let wordLabel = UILabel()
wordLabel.text = word + " "
wordLabel.font = sbFont
wordLabel.textColor = .gray
wordLabel.sizeToFit()
wordLabel.frame.origin = .init(x: currentX, y: baseY)
currentX += wordLabel.frame.width
toShowLabels.append(wordLabel)
searchBar.addSubview(wordLabel)
}
// we've added both sets of labels...
// so now we need to set the
// starting y-positions of the toShow labels
// and the
// ending y-positions of the toHide labels
// the toShow labels will be animated down *to* the baseY
// and faded in
yOffset = showIncrement
for wordLabel in toShowLabels {
wordLabel.alpha = 0.0
wordLabel.frame.origin.y = baseY - yOffset
yOffset += showIncrement
}
// the toHide labels will be animated down *from* the baseY
// and faded out
yOffset = baseY + hideIncrement
for wordLabel in toHideLabels {
wordLabel.alpha = 1.0
wordLabel.frame.origin.y = baseY
targetY.append(yOffset)
yOffset += hideIncrement
}
// clear the current placeholder text
self.searchBar.placeholder = ""
// we need to reverse the order of the toHide labels
// because we want the animation to start at the right-end
toHideLabels.reverse()
targetY.reverse()
var totalDur: Double = 0.0
var relStart: Double = 0.0
// all times in seconds (0.1 == 1/10th of a second, 0.05 = 1/20th, etc)
var hideSpeed: Double = 0.1
var hideInterval: Double = 0.05
// pause between hiding the "current" placeholder and showing the "new" placeholder
var pause: Double = 0.5
var showSpeed: Double = 0.2
var showInterval: Double = 0.1
let numToHide: Double = Double(toHideLabels.count)
let numToShow: Double = Double(toShowLabels.count)
// total duration in seconds
totalDur = (numToHide * hideSpeed) + pause + (numToShow * showSpeed)
// keyFrame start times and durations are relative to the total duration
// so convert them to percentages
hideSpeed /= totalDur
hideInterval /= totalDur
pause /= totalDur
showSpeed /= totalDur
showInterval /= totalDur
UIView.animateKeyframes(withDuration: totalDur, delay: 0, options: [.calculationModeLinear], animations: {
for (index, wordLabel) in toHideLabels.enumerated() {
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: hideSpeed, animations: {
wordLabel.alpha = 0.0
wordLabel.frame.origin.y = targetY[index]
})
relStart += hideInterval
}
relStart += pause
for wordLabel in toShowLabels {
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: showSpeed, animations: {
wordLabel.alpha = 1.0
wordLabel.frame.origin.y = baseY
})
relStart += showInterval
}
}, completion: { _ in
// set the new placeholder text
self.searchBar.placeholder = showPlaceholder
// remove all of the word labels
for wordLabel in toShowLabels {
wordLabel.removeFromSuperview()
}
for wordLabel in toHideLabels {
wordLabel.removeFromSuperview()
}
})
}
}
Looks like this when running: