PJPickerView 组件开发总结

今天周日继续撸码,继续完成另外一个组件,给之取名为——PJPickerView,别觉得它真的只是个View 哦,为了让它看上去显得不是太“重”,从而取了这个名字,本质上是个 UIViewController,可能你会以为有些奇怪,为何一个组件要上 UIViewController 呢?刚开始我也不想这么玩,听我慢慢道来。git

UI

仍是先来看 UI,github

UI 已经画得十分清楚了,就是要让咱们分离出一个组件来,并且仍是可以自定义数据源的。windows

思考

  • 确定要用到 UIPickerViewUIDatePickerView ,只不过须要在 UIPickerView 上自定义一下;
  • 要处理好蒙版。若是这还像以前那般偷懒,直接把整个组件添加到当前控制器视图上,蒙版的显示区域只能是 UINavigationBar 下的区域,这样会少了头部遮罩,十分奇怪;若是是把组件添加到当前显示的 UIWindow 上,那么 statusBar 里的运营商、电量和时间等信息也不会被遮罩,并且会异常明显的被高亮出来,若是你感兴趣的话,能够尝试把一个黑色的 UIView 直接添加到当前 UIWindow 上。
  • 由于是个组件,因此是确定不能走代理回调的。第一,Apple 自家的各类系统组件基本上都走的代理回调,再多写几个代理给本身或者其它人调用估计得炸了;第二,这但是高大上的 Swift,怎么还能屈服于老土的 Objective-C 时代的各类回调呢?闭包是必定要闭的!

实践

自定义 UIPickerView

UIPickerView 的各类回调使用方式和流程与 UITableView 及其相似,一样须要继承 UIPickerViewDelegate, UIPickerViewDataSource,并实现如下几个方法便可:api

// MARK: - Delegate
func numberOfComponents(in pickerView: UIPickerView) -> Int {
    // 告诉 UIPickerView 有多少组
}

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    // 告诉 UIPickerView 每组下有多少条数据,component 为组别
}

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    // 返回 UIPickerView 每组下每条数据须要显示的内容,只能是字符串,若是要自定义 View 走 `pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView` 这个方法
}

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    // 拿到 UIPickerView 当前组别和条数,至关于 section 和 row,注意:若是用户什么都选,默认在第一条,但此时由于用户并未进行操做,因此该代理方法里写的内容不会被执行
}
复制代码

只要按照对应代理方法所提供的做用填写代码便可,由于 PJPickerView 最多只作两组数据,因此直接拿了一个二维数组去作了数据源,固然,若是调用者非得塞下超过两列的内容也不是不行,但显示出来的效果就会畸变,目前我除了再自定义一个数据源模型替代二维字符串数组外没有更好的想法。数组

闭包回调

在以前很长的一段时间里,我很是喜欢用代理回调作组件间,甚至 vc 间的事件处理回调,可能由于当时以为这是最简单的一种方式了吧,到今年这段时间强制性压迫本身且到 Swift 上,若是在 Swift 上还用 OC 那一套流程去写代理回调,出来的效果全是浓浓到 OC 味道,一点都不 Swifty闭包

因此,我采用以下方式来进行处理回调:app

// 声明一个中间闭包,做为后边逃逸闭包的引用
private var complationHandler: ((String) -> Void)?

// ...

// MARK: - Public
class func showPickerView(viewModel: ((_ model: inout PickerModel) -> Void)?, complationHandler: @escaping (String) -> Void) {
    let picker = PJPickerView()
    picker.viewModel = PickerModel()
    if viewModel != nil {
        viewModel!(&picker.viewModel!)
        picker.initView()
    }
        
    picker.complationHandler = complationHandler
    // 这是重点方法,后文讲解
    picker.showPicker()
}
复制代码

由于涉及到许多变量,因此在此我用了一个结构体去作了承载:学习

struct PickerModel {
    var pickerType: pickerType = .time
    var dataArray = [[String]]()
    var titleString = ""
}

enum pickerType {
    case time
    case custom
}
复制代码

不想在外部调用初始化器对 PJPickerView 作初始化,采用了类方法供外部调用,且在类方法内部对 viewModel 作初始化,经过 inout 关键字修改其为可变参数传出给外部,这样就能够达到在外部对 viewModel 设置好相关参数后,在类内部直接使用便可。ui

最后使用 @escaping 关键字把跟随的闭包设置为了逃逸闭包,用以前声明的 complationHandler 对该逃逸闭包进行引用,供对应方法进行调用,调用方式所示:spa

@objc fileprivate func okButtonTapped() {
   
    // ...

    // finalString 为 UIPickerView 选中的字符串,在 didSelectRow 方法进行设置
    if complationHandler != nil {
        complationHandler!(finalString)
    }
}
复制代码

这样就完成了当对 UIPickerView 进行选择时能够回调给调用方,而调用方能够这么来进行调用:

PJPickerView.showPickerView(viewModel: { (viewModel) in
    viewModel.titleString = "感情状态"
    viewModel.pickerType = .custom
    viewModel.dataArray = [["单身", "约会中", "已婚"]]
}) { [weak self] finalString in
    if let `self` = self {
        self.loveTextField.text = finalString
    }
}
复制代码

以上的这种调用方式就是为心里中相对较为完美的调用方法了!🤓

蒙版

通过以上几个步骤后,咱们基本上已经把 UIPickerView 的主体搭建完毕,接下来进行蒙版的设计。

若是此时咱们把 PJPickerView 带上蒙版(实际就是个 UIView)直接添加到 ViewController.view 上,蒙版只会占据 ViewController.view.frame 的区域,若是当前的这个 ViewControllerUINavigationBar 下,会致使头部区域没法被蒙版覆盖,因此是确定不能直接添加到 ViewController 上的。

以前个人偷懒作法是直接把组件添加到当前 topWindow 上,这样就可以除了顶部状态栏上之外全覆盖了,但问题是若是咱们就想把包括顶部状态栏也一块儿覆盖掉呢?此时直接用 UIApplications 里的 UIWindow,好比这么把最上层 UIWindow 拿出来:

+ (UIWindow *)TopWindow {
    UIWindow * window = [[UIApplication sharedApplication].delegate window];
    if ([[UIApplication sharedApplication] windows].count > 1) {
        NSArray *windowsArray = [[UIApplication sharedApplication] windows];
        window = [windowsArray lastObject];
    }
    return window;
}
复制代码

默认状况且咱们不作其它任何修改,这样拿到的 UIWindowwindowLevelnormal,而咱们的状态栏所在的 UIWindowstatusBar 级别, UIWindowLevel 的三种级别排序为:normal < statusBar < alert,因此这才会出现了若是咱们直接把组件添加到当前 UIWindow 上蒙版并不能覆盖到顶部状态栏部分。

因此解决办法时,再造一个 UIWindow.Level == .alertUIWindow 做为组件的容器,为了更好的让 UIWindow 对组件进行管理,此时也就引出了为何 PJPickerView 底层是个 UIViewController 而不是 UIView 的缘由:

private func initView() {
    // 把当前 window 拿到
    mainWindow = windowFromLevel(level: .normal)
    pickerWindow = windowFromLevel(level: .alert)
    
    if pickerWindow == nil {
        pickerWindow = UIWindow(frame: UIScreen.main.bounds)
        pickerWindow?.windowLevel = .alert
        pickerWindow?.backgroundColor = .clear
    }
    pickerWindow?.rootViewController = self
    pickerWindow?.isUserInteractionEnabled = true

    // ...
}

func windowFromLevel(level: UIWindow.Level) -> UIWindow? {
    let windows = UIApplication.shared.windows
    for window in windows {
        if (level == window.windowLevel) {
            return window
        }
    }
    return nil
}

// show 方法
private func showPicker() {
    pickerWindow?.makeKeyAndVisible()

    // ...
}

// MARK: - Actions
    @objc fileprivate func dismissView() {
        UIView.animate(withDuration: 0.25, animations: {
            // ...
        }) { (finished) in
            if finished {
                UIView.animate(withDuration: 0.25, animations: {
                    self.pickerWindow?.isHidden = true
                    self.pickerWindow?.removeFromSuperview()
                    self.pickerWindow?.rootViewController = nil
                    self.pickerWindow = nil
                }, completion: { (finished) in
                    if finished {
                        self.mainWindow?.makeKeyAndVisible()
                    }
                })
            }
        }
    }

复制代码

成果

总结

在实现 PJPickerView 的过程当中,第一场较为完整的学习和经历了如下事情: ·

  • 自定义 UIPickerView
  • 简单的闭包回调的设计;
  • 对蒙版的思考;

总的来讲在实现的过程当中本身主要是在反思“高内聚,低耦合”的指导,以前的作法都太简单粗暴,并且太过啰嗦,第一次较为完整的思考了整个流程,确定仍是有不足之处,等到后续功力慢慢增加再来对它好好修补一翻吧~

只放出了部分核心代码,不保证可以彻底复现,只提供个思路~无论怎么说这周末的过的很开心,把手上的事情又往前推动了一大步!

原文地址:PJ 的 iOS 开发之路

相关文章
相关标签/搜索