今天週日繼續擼碼,繼續完成另外一個組件,給之取名爲——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 開發之路