iOS: 实现一个自己的选择器

为什么要写个这样的选择器

开发中, 我们经常需要用到选择器, 比如时间的选择, 地址的选择等等, 我们不能每一次都是重复写类似的代码来实现不同的效果, 这样不仅效率低, 也难以维护(可以通过中间者去接收不同的参数来生成不同的PickerView), 这里我只是通过构造函数去初始化不同的PickerView.


这里我选取的例子是地址选择器, 通过传入指定格式的数据就可以动态生成不同的PickerView

以上是最后的实现效果, 来看一下实现过程.

实现思路

归根结底, 动态的生成PickerView只不过是数据的不同导致的, 那么数据源肯定是从外界传入的(这个地方我开始想的是能不能数据格式都任意, 这样可以达到最大程度的自由, 但是被现实打脸了, 在实现PickerViewDatasource方法的时候, 无法正确匹配上, 除非交给外界类去实现, 但是代码量其实没有减少, 最终选择了在规定了数据格式的前提下最大程度的实现自由度, 大家如果有好的方法教教我).

我设置的数据格式是:

mainContent: 代表第一个Component的数据, 如果只有一个Component, 那么只需要设置这一个数据就够了.
subContents: 后面Component 的数据, 可选类型, 有多个区域则需要设置, 后续的判断都会基于这个值是否是nil.

mainContent 是一个一维的 String 数组.
subContents是一个[[String : [String]]]的复合结构, 它的每一个维度代表一个Component, 它的每一个key是前一个Component的值, 这样就可以最大程度的自由化了, 只要数据格式满足, 就可以实现(我能想到的).

代码部分

1. 找到每一个位置对应的值

      // 获取选中值的方法
    fileprivate func getSelectedValue(with component : Int) -> String {
        var value : String!
        
        value = self.mainContent[self.selectedIndexes[component]]
        
        if component == 0 {
            value = self.mainContent[self.selectedIndexes[component]]
        }else{
            // 前一个模块的选中值
            for index in 1..<component {
                value = self.subContents![index - 1][value]![self.selectedIndexes[index]]
            }
            value = self.subContents![component - 1][value]![self.selectedIndexes[self.selectedIndexes.count - 1]]
        }
        return value
    }

通过循环的方式去取出对应Component的值, 因为第一个Component是直接可以取到的, 后面的Component展示的是以前一个Component的值为Key的字典, 这样, 我们就能获取到了.

2. PickerViewDataSource选中刷新

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // 设置选中
        self.selectedIndexes[component] = row
        // 前面的部分刷新都需要更新后面的Component
        if self.subContents != nil {
            let startIndex = component + 1
            for refreshIndex in startIndex ..< self.subContents!.count + 1 {
                pickerView.reloadComponent(refreshIndex)
                pickerView.selectRow(0, inComponent: refreshIndex, animated: true)
            }
        }
    }

首先, 进行判断, 如果只有mainContent的情况下, 就只需要保存一下选中的位置即可, 如果subContents 存在, 则需要进行循环设置值(这里的逻辑和上方取值是类似的).

3. 动画

    fileprivate func initAnimation() {
        /** 出现动画 */
        // 缩放动画
        let sizeShowAnimation = CAKeyframeAnimation.init(keyPath: "position.y")
        sizeShowAnimation.values = [self.bounds.height + self.desView!.bounds.height, self.bounds.height, self.bounds.height - (pickerHeight + buttonSize + 10.0)]
        
        sizeShowAnimation.autoreverses = false
        
        sizeShowAnimation.timingFunctions = [CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.easeOut),CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.default)]
        
        sizeShowAnimation.fillMode = CAMediaTimingFillMode.forwards
        
        sizeShowAnimation.calculationMode = CAAnimationCalculationMode.linear
        
        sizeShowAnimation.isRemovedOnCompletion = false
        // 透明度动画
        let alphaShowAnimation = CABasicAnimation.init(keyPath: "opacity")
        alphaShowAnimation.fromValue = 0
        alphaShowAnimation.toValue = 1
        
        self.showAnimation.animations = [sizeShowAnimation, alphaShowAnimation]
        self.showAnimation.isRemovedOnCompletion = false
        self.showAnimation.duration = 0.5
        /** 消失动画 */
        // 缩放动画
        let sizeDismissAnimation = CAKeyframeAnimation.init(keyPath: "position.y")
        
        sizeDismissAnimation.values = [self.bounds.height - self.desView!.bounds.height, self.bounds.height - buttonSize, self.bounds.height + self.desView!.bounds.height]
        
        sizeDismissAnimation.keyTimes = [0.1, 0.7, 1.0]
        
        sizeDismissAnimation.autoreverses = false
        
        sizeDismissAnimation.timingFunctions = [CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.easeOut),CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.default)]
        
        sizeShowAnimation.fillMode = CAMediaTimingFillMode.forwards
        
        sizeShowAnimation.calculationMode = CAAnimationCalculationMode.linear
        
        sizeShowAnimation.isRemovedOnCompletion = false
        // 透明度动画
        let alphaDismissAnimation = CABasicAnimation.init(keyPath: "opacity")
        alphaDismissAnimation.fromValue = 1
        alphaDismissAnimation.toValue = 0
        
        // 设置基本属性
        self.dismissAnimation.animations = [sizeDismissAnimation, alphaDismissAnimation]
        self.dismissAnimation.isRemovedOnCompletion = false
        self.dismissAnimation.duration = 0.5
        self.dismissAnimation.delegate = self
        self.dismissAnimation.setValue("dismiss", forKey: "type")
    }

动画这里我只是简单写了一下, 这一部分比较好修改, 使用的Group组合了平移和透明度变换的动画, 这里我设置了代理, 主要是为了在结束的时候将View从控件中去除.

4. 初始化

// 初始化选中数组, 默认全是第一个
        var total = 1
        if subContents != nil {
            total = self.subContents!.count + 1
        }
        
        for _ in 0..<total {
            self.selectedIndexes.append(0)
        }
        if tap {
            self.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(MXSelectableView.tapForHidden(_:))))
        }
        // 通过颜色设置透明度
        self.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.3);
        // 初始化Container
        self.initContainer()
        // 初始化控件
        self.initViews()

在这里, 就已经将控件初始化出来, 添加是在show()的时候, 才会将当前的控件添加到keywindow中去, 在dismiss()动画结束的时候从keywindow 中去除.

5. 调用

        if self.picker != nil && self.view.subviews.contains(self.picker) {
            return
        }
        // mainContent: self.provinces, subContents: [self.cities, self.areas], isGroup: true
        self.picker = MXSelectableView.init(mainContent: provinces, subContents: [cities, areas], isGroup: true, selectedCallBack: { (result : [String]) in
            print(result)
        })
  
        picker.show()

只需要设置相关的属性, 就可以得到对应的pickerView, 然后调用show()/dismiss()用于展示和隐藏.

详细代码在: Github

总结

以后写代码的时候也应该多考虑一下, 当前做的东西是否可以作为可复用的模块, 尝试将组件化思维运用起来, 并去尝试使用中间者模式进行组件之间的管理, 这样可以最大程度的提高自己的效率以及减少维护代价.

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

推荐阅读更多精彩内容

  • UIPickerView也是一个选择器控件,它比UIDatePicker更加通用,它可以生成单列的选择器,也可生成...
    小蘑菇2阅读 3,556评论 3 5
  • 废话不多说,直接上干货 ---------------------------------------------...
    小小赵纸农阅读 3,333评论 0 15
  • 《裕语言》速成开发手册3.0 官方用户交流:iApp开发交流(1) 239547050iApp开发交流(2) 10...
    叶染柒丶阅读 25,956评论 5 19
  • 一、简介 <<UIPickerView类实现对象,所谓的选择器的看法,即使用一个纺车或老虎机的比喻来显示一个或多个...
    无邪8阅读 2,529评论 0 3
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32