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 開發之路

相關文章
相關標籤/搜索