UIKit中如何实现占位符动态切换动画?现有代码效果不符求修正
问题描述
希望实现占位符文字逐个动态切换的动画效果(如参考示例所示):
当前代码尝试通过上下移动标签实现动画,但实际效果与预期差距较大:
原实现代码如下:
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 } }
修正方案
原代码的核心问题是采用了整体标签上下移动的逻辑,与目标的逐个字符替换动画不匹配。我们需要通过逐字符构建/删除文本的方式实现效果,修改后的代码如下:
import UIKit class ViewController: UIViewController, UISearchBarDelegate { var placeholderIndex = 0 var placeholderTexts = ["Text 1", "Text 2", "Text 3"] var searchBar: UISearchBar! // 用于控制动画过程中的文本构建状态 var currentTextBuilder = "" var currentTextIndex = 0 override func viewDidLoad() { super.viewDidLoad() searchBar = UISearchBar() searchBar.placeholder = placeholderTexts.first ?? "" searchBar.searchBarStyle = .prominent searchBar.sizeToFit() searchBar.isTranslucent = true searchBar.delegate = self searchBar.backgroundImage = UIImage() searchBar.translatesAutoresizingMaskIntoConstraints = false view.addSubview(searchBar) // 启动第一轮动画序列 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.startCharacterReplacementAnimation() } 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) ]) } /// 启动逐个字符替换的完整动画序列 func startCharacterReplacementAnimation() { let targetText = placeholderTexts[placeholderIndex] let currentText = searchBar.placeholder ?? "" // 先执行文本清空动画,再执行文本构建动画 if !currentText.isEmpty { clearTextWithAnimation { self.buildTextWithAnimation(targetText: targetText) } } else { buildTextWithAnimation(targetText: targetText) } // 更新索引,准备下一轮动画 placeholderIndex = (placeholderIndex + 1) % placeholderTexts.count // 3秒后启动下一轮动画循环 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.startCharacterReplacementAnimation() } } /// 逐个删除现有文本的动画逻辑 func clearTextWithAnimation(completion: @escaping () -> Void) { guard var currentText = searchBar.placeholder, !currentText.isEmpty else { completion() return } let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in guard let self = self else { return } currentText.removeLast() self.searchBar.placeholder = currentText if currentText.isEmpty { timer.invalidate() completion() } } timer.fire() } /// 逐个添加目标文本的动画逻辑 func buildTextWithAnimation(targetText: String) { currentTextBuilder = "" currentTextIndex = 0 let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in guard let self = self else { return } if self.currentTextIndex < targetText.count { let index = targetText.index(targetText.startIndex, offsetBy: self.currentTextIndex) self.currentTextBuilder.append(targetText[index]) self.searchBar.placeholder = self.currentTextBuilder self.currentTextIndex += 1 } else { timer.invalidate() } } timer.fire() } }
关键修改说明
- 替换原有的整体标签移动逻辑,改为逐字符删除+逐字符添加的分步动画,完全匹配目标效果
- 使用定时器控制每个字符的动画间隔(0.1秒),保证动画流畅自然
- 采用异步延迟调度替代重复Timer,避免动画重叠问题
- 拆分清空和构建两个动画阶段,实现完整的占位符切换流程
内容的提问来源于stack exchange,提问作者Swarup Panda
相关产品推荐
相关产品推荐

