如何实现UITextField上浮至导航栏转为UISearchController的自定义动画?
这个需求我之前也做过,要实现类似iOS文件应用那种文本框上浮变形为搜索栏的过渡效果,核心是抛弃系统默认的present动画,改用自定义转场动画来实现。下面我给你一步步拆解实现思路和代码:
核心思路
我们需要创建一个自定义的转场动画控制器,捕捉原文本框的位置和样式,通过快照视图(snapshot)模拟文本框上浮、变形为搜索栏的过程,同时渐显目标搜索控制器的视图,完全替代系统默认的下拉动画。
第一步:实现自定义转场动画类
这个类负责处理动画的具体逻辑,包括坐标转换、快照创建、动画过渡等:
class TextFieldToSearchBarTransition: NSObject, UIViewControllerAnimatedTransitioning { private let originTextField: UITextField private let animationDuration: TimeInterval = 0.35 // 初始化时传入触发动画的原文本框 init(originTextField: UITextField) { self.originTextField = originTextField super.init() } // 动画时长 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return animationDuration } // 核心动画逻辑 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let targetSearchVC = transitionContext.viewController(forKey: .to) as? UISearchController, let searchBar = targetSearchVC.searchBar, let containerView = transitionContext.containerView else { transitionContext.completeTransition(false) return } // 1. 转换坐标:获取原文本框的屏幕位置,以及搜索栏在容器中的最终位置 let originFrame = originTextField.convert(originTextField.bounds, to: nil) let finalSearchBarFrame = containerView.convert(searchBar.bounds, from: searchBar.superview) // 2. 创建原文本框的快照,作为动画载体 let textFieldSnapshot = originTextField.snapshotView(afterScreenUpdates: true)! textFieldSnapshot.frame = originFrame containerView.addSubview(textFieldSnapshot) // 3. 临时隐藏原文本框和搜索栏,避免干扰动画视觉 originTextField.isHidden = true searchBar.isHidden = true // 4. 将搜索控制器的视图添加到容器,初始设为透明 containerView.addSubview(targetSearchVC.view) targetSearchVC.view.alpha = 0 // 5. 执行过渡动画:快照移动+变形,同时渐显目标视图 UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) { textFieldSnapshot.frame = finalSearchBarFrame textFieldSnapshot.layer.cornerRadius = searchBar.layer.cornerRadius textFieldSnapshot.layer.masksToBounds = true targetSearchVC.view.alpha = 1 } completion: { _ in // 动画结束后清理:显示搜索栏,移除快照,标记动画完成 searchBar.isHidden = false textFieldSnapshot.removeFromSuperview() transitionContext.completeTransition(true) } } }
第二步:修改搜索控制器支持自定义转场
让PlacesSearchController遵循UIViewControllerTransitioningDelegate,提供我们的自定义动画:
class PlacesSearchController: UISearchController, UISearchBarDelegate, UIViewControllerTransitioningDelegate { private var customTransition: TextFieldToSearchBarTransition? // 修改便利初始化方法,传入原文本框 convenience init(delegate: PlacesAutocompleteViewControllerDelegate, originTextField: UITextField) { let tableViewController = PlacesAutocompleteContainer(delegate: delegate) self.init(searchResultsController: tableViewController) self.searchResultsUpdater = tableViewController self.hidesNavigationBarDuringPresentation = false self.definesPresentationContext = true self.searchBar.placeholder = searchBarPlaceholder // 初始化自定义转场动画 self.customTransition = TextFieldToSearchBarTransition(originTextField: originTextField) self.transitioningDelegate = self } // 提供present时的自定义动画 func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return customTransition } // 可选:如果需要实现dismiss时的反向动画(搜索栏下沉回原文本框),可以在这里返回对应的反向动画类 func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil } }
第三步:修改触发逻辑
在ShowAddressViewController中,初始化搜索控制器时传入原文本框,触发present:
private extension ShowAddressViewController { @objc func streetAddressTextFieldEditingDidBegin() { // 初始化时传入触发动画的文本框 let searchController = PlacesSearchController(delegate: self, originTextField: streetAddressTextField) // 同步原文本框的内容到搜索栏,过渡更连贯 searchController.searchBar.text = streetAddressTextField.text present(searchController, animated: true, completion: nil) } }
额外优化建议
- 确保原文本框和搜索栏的样式(比如圆角、高度、字体)尽量接近,动画过渡会更自然;
- 如果需要支持dismiss时的反向动画,可以仿照present的逻辑,创建一个反向的转场类,让搜索栏的快照下沉回原文本框位置;
- 测试不同屏幕尺寸的适配,确保坐标转换的准确性。
内容的提问来源于stack exchange,提问作者TruMan1
相关产品推荐
相关产品推荐

