今天周日继续撸码,继续完成另外一个组件,给之取名为——PJPickerView
,别觉得它真的只是个View
哦,为了让它看上去显得不是太“重”,从而取了这个名字,本质上是个 UIViewController
,可能你会以为有些奇怪,为何一个组件要上 UIViewController
呢?刚开始我也不想这么玩,听我慢慢道来。git
仍是先来看 UI,github
UI 已经画得十分清楚了,就是要让咱们分离出一个组件来,并且仍是可以自定义数据源的。windows
UIPickerView
和 UIDatePickerView
,只不过须要在 UIPickerView
上自定义一下;UINavigationBar
下的区域,这样会少了头部遮罩,十分奇怪;若是是把组件添加到当前显示的 UIWindow
上,那么 statusBar
里的运营商、电量和时间等信息也不会被遮罩,并且会异常明显的被高亮出来,若是你感兴趣的话,能够尝试把一个黑色的 UIView
直接添加到当前 UIWindow
上。Swift
,怎么还能屈服于老土的 Objective-C
时代的各类回调呢?闭包是必定要闭的!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
的区域,若是当前的这个 ViewController
在 UINavigationBar
下,会致使头部区域没法被蒙版覆盖,因此是确定不能直接添加到 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;
}
复制代码
默认状况且咱们不作其它任何修改,这样拿到的 UIWindow
的 windowLevel
是 normal
,而咱们的状态栏所在的 UIWindow
是 statusBar
级别, UIWindowLevel
的三种级别排序为:normal
< statusBar
< alert
,因此这才会出现了若是咱们直接把组件添加到当前 UIWindow
上蒙版并不能覆盖到顶部状态栏部分。
因此解决办法时,再造一个 UIWindow.Level == .alert
的 UIWindow
做为组件的容器,为了更好的让 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 开发之路