PJPickerView 元件開發總結
今天週日繼續擼碼,繼續完成另一個元件,給之取名為—— PJPickerView
,別以為它真的只是個 View
哦,為了讓它看上去顯得不是太“重”,從而取了這個名字,本質上是個 UIViewController
,可能你會覺得有些奇怪,為什麼一個元件要上 UIViewController
呢?剛開始我也不想這麼玩,聽我慢慢道來。
UI
還是先來看 UI,

UI 已經畫得十分清楚了,就是要讓我們分離出一個元件來,而且還是能夠自定義資料來源的。
思考
- 肯定要用到
UIPickerView
和UIDatePickerView
,只不過需要在UIPickerView
上自定義一下; - 要處理好蒙版。如果這還像之前那般偷懶,直接把整個元件新增到當前控制器檢視上,蒙版的顯示區域只能是
UINavigationBar
下的區域,這樣會少了頭部遮罩,十分奇怪;如果是把元件新增到當前顯示的UIWindow
上,那麼statusBar
裡的運營商、電量和時間等資訊也不會被遮罩,而且會異常明顯的被高亮出來,如果你感興趣的話,可以嘗試把一個黑色的UIView
直接新增到當前UIWindow
上。 - 因為是個元件,所以是肯定不能走代理回撥的。第一,Apple 自家的各種系統元件基本上都走的代理回撥,再多寫幾個代理給自己或者其它人呼叫估計得炸了;第二,這可是高大上的
Swift
,怎麼還能屈服於老土的Objective-C
時代的各種回撥呢?閉包是一定要閉的!
實踐
自定義 UIPickerView
UIPickerView
的各種回撥使用方式和流程與 UITableView
及其類似,同樣需要繼承 UIPickerViewDelegate, UIPickerViewDataSource
,並實現以下幾個方法即可:
// 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
。
所以,我採用如下方式來進行處理回撥:
// 宣告一箇中間閉包,作為後邊逃逸閉包的引用 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
設定好相關引數後,在類內部直接使用即可。
最後使用 @escaping
關鍵字把跟隨的閉包設定為了逃逸閉包,用之前宣告的 complationHandler
對該逃逸閉包進行引用,供對應方法進行呼叫,呼叫方式所示:
@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
總的來說在實現的過程中自己主要是在反思“高內聚,低耦合”的指導,之前的做法都太簡單粗暴,而且太過囉嗦,第一次較為完整的思考了整個流程,肯定還是有不足之處,等到後續功力慢慢增長再來對它好好修補一翻吧~
只放出了部分核心程式碼,不保證能夠完全復現,只提供個思路~不管怎麼說這週末的過的很開心,把手上的事情又往前推進了一大步!
原文地址: iOS-Course%2Fblob%2Fmaster%2FiOS%2FSwift%2FPJPickerView%25E5%25BC%2580%25E5%258F%2591%25E6%2580%25BB%25E7%25BB%2593.md" rel="nofollow,noindex">PJ 的 iOS 開發之路