Swift 5 防高德地图搜索

一款好的搜索展示动画,离不开前人的辛勤劳动

老规矩,先上效果图:


demo.gif

可搜索,可滑动,是不是足够满足你的需要😎

实现

  1. 首先你需要一个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
    }()
  1. 有了列表,我们还需要什么呢?🤔那当然是搜索框啦
    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
    }()
  1. 有了列表和搜索框当然是开始完善我们的搜索页啦,填数据什么的我想你们都在话下了,就不写了,主要是讲下列表如何关联搜索框。
    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
    }
  1. 那么好,到这里我们已经把搜索视图准备好了,现在该处理列表的滑动事件。根据上下滑动事件获取相对应的停止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
    }
  1. 动态改变tableiView的高度
    fileprivate var offsetY: CGFloat = Y2 {
        didSet {
            tableView.frame.size.height = view.bounds.height - offsetY
        }
    }

讲到这里就需要提到上文的手势代理干嘛用了🤭

  1. 首先在滑动的时候取消搜索状态,让搜索框跟随滚动
  2. 根据列表状态判断是否让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
    }

有同学可能会有疑问了,我点击搜索框时为什么会出现上弹动画呢,这里就给你们讲解一哈。不知道大家有没有注意到在我们设置UISearchControllersearchBar属性时,我们设置了搜索条的代理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
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343