一款好的搜索展示动画,离不开前人的辛勤劳动
老规矩,先上效果图:
可搜索,可滑动,是不是足够满足你的需要😎。
实现
- 首先你需要一个
TableView
,并且监听上下滑动手势。⚠️需要设置滑动事件的代理
为什么捏,你先想想😄
fileprivate lazy var tableView: UITableView = {
let table = let table = UITableView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height - Y2), style: .plain)
table.delegate = self
table.dataSource = self
table.bounces = false
table.isScrollEnabled = false
table.tableFooterView = UIView()
let down = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
down.direction = .down
down.delegate = self
table.addGestureRecognizer(down)
let up = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
up.direction = .up
up.delegate = self
table.addGestureRecognizer(up)
return table
}()
- 有了列表,我们还需要什么呢?🤔那当然是搜索框啦
fileprivate lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.placeholder = "搜索"
searchController.searchBar.searchBarStyle = .minimal
searchController.searchBar.barTintColor = .white
// 去掉searchBar上下的两条黑线 这里需要设置任意的图片去覆盖黑线
searchController.searchBar.setBackgroundImage(UIColor.clear.jx_toImage(size: CGSize(width: 1, height: 1)), for: .any, barMetrics: .default)
searchController.searchBar.sizeToFit()
// 设置开始搜索时背景显示与否
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.delegate = self
return searchController
}()
- 有了列表和搜索框当然是开始完善我们的搜索页啦,填数据什么的我想你们都在话下了,就不写了,主要是讲下列表如何关联搜索框。
UISearchBar
默认高度为44,把它放在一个自定义的searchView,通过设置searchView在backgroundView中的位置,让它看起来好像是searchBar的高度在改变😎
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let width = tableView.bounds.width
let backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: width, height: 20 + 44))
backgroundView.backgroundColor = .white
let searchView = UIView(frame: CGRect(x: 0, y: 15, width: width, height: 44))
searchView.addSubview(searchController.searchBar)
backgroundView.addSubview(searchView)
let hintV = UIView(frame: CGRect(x: (width - 40) / 2.0, y: 10, width: 40, height: 4))
hintV.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
hintV.layer.cornerRadius = hintV.bounds.height/2
hintV.layer.masksToBounds = true
backgroundView.addSubview(hintV)
return backgroundView
}
- 那么好,到这里我们已经把搜索视图准备好了,现在该处理列表的滑动事件。根据上下滑动事件获取相对应的停止Y坐标,by the way,手动加了个回弹动画👻
// table可滑动时,swipe默认不再响应 所以要打开
@objc func swipe(_ swipe: UISwipeGestureRecognizer) {
guard let shadowView = self.shadowView else {
return
}
var stopY: CGFloat = 0
var animateY: CGFloat = 0
let margin: CGFloat = 10 // 动画的幅度
let offsetY = shadowView.frame.origin.y // 这是上一次Y的位置
if swipe.direction == .down {
// 当vc.table滑到顶部且是下滑时,让vc.table禁止滑动
if tableView.contentOffset.y == 0 {
tableView.isScrollEnabled = false
}
if offsetY >= Y1 {
// 停在Y2的位置
stopY = Y2
} else {
stopY = Y1
}
animateY = stopY + margin
}
if swipe.direction == .up {
if offsetY <= Y2 {
stopY = Y1
// 当停在Y1位置且是上划时,让vc.table不再禁止滑动
tableView.isScrollEnabled = true
} else {
stopY = Y2
}
animateY = stopY - margin
}
// 弹性动画
let bounce = CGRect(x: 0, y: animateY, width: view.bounds.width, height: mScreenH)
let to = CGRect(x: 0, y: stopY, width: bounce.width, height: bounce.height)
UIView.animate(withDuration: 0.4, animations: {
shadowView.frame = bounce
}) { finished in
UIView.animate(withDuration: 0.2, animations: {
shadowView.frame = to
})
}
// 记录shadowView在第一个视图中的位置
self.offsetY = stopY
}
- 动态改变
tableiView
的高度
fileprivate var offsetY: CGFloat = Y2 {
didSet {
tableView.frame.size.height = view.bounds.height - offsetY
}
}
讲到这里就需要提到上文的手势代理干嘛用了🤭
- 首先在滑动的时候取消搜索状态,让搜索框跟随滚动
- 根据列表状态判断是否让swipe响应手势事件
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
cancelSearch()
// 当table允许滚动且offsetY不为0时,让swipe响应
if tableView.isScrollEnabled == true && tableView.contentOffset.y != 0 {
return false
}
if tableView.isScrollEnabled == true {
return true
}
return false
}
有同学可能会有疑问了,我点击搜索框时为什么会出现上弹动画呢,这里就给你们讲解一哈。不知道大家有没有注意到在我们设置UISearchController
的searchBar
属性时,我们设置了搜索条的代理delegate
,这就是啦
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
// 如果点击时,shadowView的y坐标 不在Y1的位置,
if offsetY > Y1+1 {
UIView.animate(withDuration: 0.4, animations: {
self.shadowView.frame = CGRect(x: 0, y: Y1, width: self.view.frame.width, height: UIScreen.main.bounds.height)
}) { finished in
// 呼出键盘。 一定要在动画结束后调用,否则会出错
self.searchController.searchBar.becomeFirstResponder()
}
// 更新offsetY
offsetY = self.shadowView.frame.origin.y
return false
}
return true
}
以上我们都是在将列表展示逻辑,那到底要怎么显示呢,莫及莫及,且听我慢慢道来(别打我🌚
相信看到这里的朋友肯定对出现在上文的shadowView
表示不理解,这个视图哪来的啊,这也是最后需要做的一步,也就是展示。
class LocationSearchVC: UIViewController {
fileprivate lazy var searchResultVC: SearchResultVC = {
let vc = SearchResultVC(shadowView)
return vc
}()
fileprivate lazy var shadowView: UIView = {
let v = UIView()
v.layer.shadowColor = UIColor.black.cgColor
v.layer.shadowRadius = 10
v.layer.shadowOffset = CGSize(width: 5, height: 5)
v.layer.shadowOpacity = 0.8
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
addChild(searchResultVC)
shadowView.addSubview(searchResultVC.view)
view.addSubview(shadowView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
searchResultVC.cancelSearch()
}
}
下面放出完整代码
import Foundation
private let Y1 = mScreenH / 3
private let Y2 = mScreenH / 3 * 2
class SearchResultVC: UIViewController {
fileprivate lazy var tableView: UITableView = {
let table = UITableView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height - Y2), style: .plain)
table.delegate = self
table.dataSource = self
table.bounces = false
table.isScrollEnabled = false
table.tableFooterView = UIView()
let down = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
down.direction = .down
down.delegate = self
table.addGestureRecognizer(down)
let up = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
up.direction = .up
up.delegate = self
table.addGestureRecognizer(up)
return table
}()
fileprivate lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.placeholder = "搜索"
searchController.searchBar.searchBarStyle = .minimal
searchController.searchBar.barTintColor = .white
// 去掉searchBar上下的两条黑线
searchController.searchBar.setBackgroundImage(UIColor.clear.jx_toImage(size: CGSize(width: 1, height: 1)), for: .any, barMetrics: .default)
searchController.searchBar.sizeToFit()
// 设置开始搜索时背景显示与否
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.delegate = self
return searchController
}()
var dataArray = ["11", "222", "3333", "abcdd", "222", "3333", "abcdd", "11", "222", "3333", "abcdd", "222", "3333", "abcdd"]
fileprivate var offsetY: CGFloat = Y2 {
didSet {
tableView.frame.size.height = view.bounds.height - offsetY
}
}
fileprivate var shadowView: UIView!
/// 当前View所在父View
init(_ parentView: UIView) {
super.init(nibName: nil, bundle: nil)
shadowView = parentView
shadowView.frame = CGRect(x: 0, y: offsetY, width: view.bounds.width, height: view.bounds.height)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.clipsToBounds = true
view.layer.cornerRadius = 10
view.addSubview(tableView)
}
/// 取消搜索
func cancelSearch() {
let cancelBtn = searchController.searchBar.value(forKey: "cancelButton") as? UIButton
cancelBtn?.sendActions(for: .touchUpInside)
}
}
fileprivate extension SearchResultVC {
// table可滑动时,swipe默认不再响应 所以要打开
@objc func swipe(_ swipe: UISwipeGestureRecognizer) {
guard let shadowView = self.shadowView else {
return
}
var stopY: CGFloat = 0
var animateY: CGFloat = 0
let margin: CGFloat = 10 // 动画的幅度
let offsetY = shadowView.frame.origin.y // 这是上一次Y的位置
if swipe.direction == .down {
// 当vc.table滑到顶部且是下滑时,让vc.table禁止滑动
if tableView.contentOffset.y == 0 {
tableView.isScrollEnabled = false
}
if offsetY >= Y1 {
// 停在Y2的位置
stopY = Y2
} else {
stopY = Y1
}
animateY = stopY + margin
}
if swipe.direction == .up {
if offsetY <= Y2 {
stopY = Y1
// 当停在Y1位置且是上划时,让vc.table不再禁止滑动
tableView.isScrollEnabled = true
} else {
stopY = Y2
}
animateY = stopY - margin
}
// 弹性动画
let bounce = CGRect(x: 0, y: animateY, width: view.bounds.width, height: mScreenH)
let to = CGRect(x: 0, y: stopY, width: bounce.width, height: bounce.height)
UIView.animate(withDuration: 0.4, animations: {
shadowView.frame = bounce
}) { finished in
UIView.animate(withDuration: 0.2, animations: {
shadowView.frame = to
})
}
// 记录shadowView在第一个视图中的位置
self.offsetY = stopY
}
}
extension SearchResultVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "SearchResultCell"
var cell = tableView.dequeueReusableCell(withIdentifier: identifier)
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: identifier)
cell?.selectionStyle = .none
cell?.backgroundColor = .white
}
cell?.textLabel?.text = dataArray[indexPath.row]
return cell!
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 20 + 44
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let width = tableView.bounds.width
let backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: width, height: 20 + 44))
backgroundView.backgroundColor = .white
let searchView = UIView(frame: CGRect(x: 0, y: 15, width: width, height: 44))
searchView.addSubview(searchController.searchBar)
backgroundView.addSubview(searchView)
let hintV = UIView(frame: CGRect(x: (width - 40) / 2.0, y: 10, width: 40, height: 4))
hintV.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
hintV.layer.cornerRadius = hintV.bounds.height/2
hintV.layer.masksToBounds = true
backgroundView.addSubview(hintV)
return backgroundView
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
cancelSearch()
}
}
extension SearchResultVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.count == 0 {
cancelSearch()
}
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
let textStr = searchBar.text ?? ""
searchBar.text = ""
// 插入路径的同时,要同步插入数据
dataArray.insert(textStr, at: 0)
let index = IndexPath(row: 0, section: 0)
tableView.insertRows(at: [index], with: .bottom)
}
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
// 如果点击时,shadowView的y坐标 不在Y1的位置,
if offsetY > Y1+1 {
UIView.animate(withDuration: 0.4, animations: {
self.shadowView.frame = CGRect(x: 0, y: Y1, width: self.view.frame.width, height: UIScreen.main.bounds.height)
}) { finished in
// 呼出键盘。 一定要在动画结束后调用,否则会出错
self.searchController.searchBar.becomeFirstResponder()
}
// 更新offsetY
offsetY = self.shadowView.frame.origin.y
return false
}
return true
}
}
extension SearchResultVC: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
cancelSearch()
// 当table允许滚动且offsetY不为0时,让swipe响应
if tableView.isScrollEnabled && tableView.contentOffset.y != 0 {
return false
}
if tableView.isScrollEnabled {
return true
}
return false
}
}