Swift 開發 wanandroid 客戶端——基類控制器BaseViewController、BaseTableViewController

這是我參與更文挑戰的第24天,活動詳情查看: 更文挑戰

每日更新,我到底在作啥?

不少朋友老是被個人標題給迷惑了,Swift?玩安卓App?都是些啥?git

我反思了一下,確實本身的標題取得不太好。加上有沒有附圖我到底在作啥,是個人失誤。github

因此我決定仍是傳一張Gif給你們看看,我都在寫一個什麼樣的東東,一個沒啥太多華麗UI,用Swift編寫的iOS App,基本上我編寫的代碼和更文算是同步的:編程

RPReplay_Final1624432101.2021-06-24 08_35_54.gif

爲何要寫基類控制器

給你們分享一個本身剛入行的經歷,我剛剛從事iOS開發。markdown

那會還在寫OC代碼,新手老是從寫UI開始的,我也不例外,因爲事先大佬也沒有叮囑寫什麼,我就開始講本身寫的Controller一個個建立,大概就是這樣網絡

@interface XXXXController : UIViewController
複製代碼

沒什麼啥毛病。ide

有天,不知道是產品仍是UI來了什麼靈感,說咱們這頁面的背景色須要作些許改動,大佬說,好的沒事,一行代碼的事。而後大佬確實改了一行代碼提交看了效果,不錯,而後就開開心心提測了。函數

因而,測試就過來了,你寫的這頁面好奇怪啊,大部分的頁面背景色都是一致的,可是有幾個怎麼都看起來有色差,怎麼回事?oop

不用多說,有色差的頁面都是我寫的,至於緣由很簡單,大佬寫的代碼都是這樣的:佈局

@interface XXXXController : BaseViewController
複製代碼

其餘頁面寫的時候都是繼承的基類控制器,只有我繼承UIViewController,大佬也沒有責怪我,由於他覺得我知道這種規則就沒和我交代,因此出了差錯,加上改改繼承基本上就解決問題,因此也不是什麼大事。post

分享個人這個經歷,其實在以後的工做中給了我不少思考:

  • 一個App中,大部分頁面的UI風格、顏色、樣式基本上都是一致,經過繼承自定義的BaseViewController能夠很快的完成基礎配置。

  • 其實不只是Controller層,有的時候包括View層,咱們須要定義一個BaseView,來進行基礎配置,若是有業務須要BaseTableViewCell等都是能夠考慮的。這個就和寫BaseModel有些類似。

  • 本身寫項目,記得要作一些基類的編寫,本身接手其餘人的項目,我也總會先讓別人給我介紹一下他們的基類。

編寫BaseViewController

基於上面的工做經歷與思考,如今咱們就來玩安卓App的BaseViewController吧:

import UIKit

import RxSwift
import RxCocoa

class BaseViewController: UIViewController {
    
    private lazy var errorImage: UIImageView = {
        let imageView = UIImageView(image: R.image.saber())
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /// 最簡單的設置統一返回按鈕的方法,全部的控制器繼承該基類便可
        let leftBarButtonItem = UIBarButtonItem(image: R.image.back(), style: .plain, target: self, action: #selector(leftBarButtonItemAction(_:)))
        navigationItem.leftBarButtonItem = (navigationController?.viewControllers.count ?? 0) > 1 ? leftBarButtonItem : nil
        navigationItem.hidesBackButton = true
        
        /// 這裏的代碼有問題,須要註釋掉
        //navigationController?.interactivePopGestureRecognizer?.delegate = nil
                
        view.backgroundColor = .white
        
        setupErrorImage()
    }
        
    @objc
    private func leftBarButtonItemAction(_ item: UIBarButtonItem) {
        navigationController?.popViewController(animated: true)
    }
    
    deinit {
        print("\(className)被銷燬了")
    }

}

//MARK:- 網絡請求錯誤頁面的配置項(待用)
extension BaseViewController {
    private func setupErrorImage() {
        view.addSubview(errorImage)
        errorImage.snp.makeConstraints { make in
            make.edges.equalTo(view)
        }
        errorImage.isHidden = true
    }
    
    func showErrorImage() {
        errorImage.isHidden = false
        view.bringSubviewToFront(errorImage)
    }
    
    func hiddenErrorImage() {
        errorImage.isHidden = true
        view.sendSubviewToBack(errorImage)
    }
}

//MARK:- 綁定
extension Reactive where Base: BaseViewController {
    
    /// 顯示網絡錯誤
    var networkError: Binder<Void> {
        return Binder(base) { vc, _ in
            vc.showErrorImage()
        }
    }
}

複製代碼

其實這樣BaseViewController作了一下幾件事情:

  • 自定義返回按鈕

    • 這裏咱們使用自定義的leftBarButtonItem去代替了系統的backButton,代碼塊中這種方式是目前我見過設置最簡單、功能不會缺失的好辦法。只要UINavigationControlle初始化方法傳入的是BaseViewController的子類便可實現。

    • 系統的側滑沒有失效。

    • 點擊leftBarButtonItem的返回事件。

    • 勘誤:上面這段話是有問題的,有大佬nlnlnull留言說,我這樣寫,會在根控制器中嘗試使用側滑手勢後,會出現異常狀況,已經驗證,確實如此。

    本身寫的代碼沒有好好驗證與追根朔源,仍是很是感謝大佬的提醒,具體地址的問題請看這篇文章:自定義leftBarButtonItem致使側滑失效

    BaseViewController中須要刪除這段代碼,在代碼塊中,刪除沒法顯示,這裏單獨說明:

    navigationController?.interactivePopGestureRecognizer?.delegate = nil

    所以,咱們還須要寫一個BaseNavigationController,來避免這個問題的發生:

    import UIKit
    
    class BaseNavigationController: UINavigationController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            interactivePopGestureRecognizer?.delegate = self
            delegate = self
        }
    }
    
    extension BaseNavigationController: UIGestureRecognizerDelegate, UINavigationControllerDelegate {
        func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
            interactivePopGestureRecognizer?.isEnabled = true
            /// 解決某些狀況下push時的假死bug,防止把根控制器pop掉
            if (navigationController.viewControllers.count == 1) {
                interactivePopGestureRecognizer?.isEnabled = false
            }
        }
    }
    
    複製代碼
  • 配置了控制器的背景色爲白色。

  • 經過RxSwift,針對網絡請求致使的頁面進行了頁面處理,這一塊因爲Rx我也是一點點學習,目前可能思路與處理都不算太好,這裏只是寫出來了。

  • 在析構函數中添加了一段打印,用於查看控制器的銷燬狀況。

以前,我也說過,玩安卓App中有不少頁面都是列表,考慮到這種狀況,編寫一個BaseTableViewController也是頗有的必要的。

編寫BaseTableViewController

首先BaseTableViewController它是繼承於BaseViewController。

同時因爲是爲了展現列表,咱們須要在裏面佈局一個UITableView。

考慮列表可能會有數據爲空的狀況,咱們須要對頁面作定製化處理,這裏我選擇使用了OC庫——DZNEmptyDataSet

import UIKit

import RxSwift
import RxCocoa

import MJRefresh
import DZNEmptyDataSet

class BaseTableViewController: BaseViewController {
    
    lazy var tableView = UITableView(frame: .zero, style: .plain)
    
    let emptyDataSetButtonTap = PublishSubject<Void>()
    
    let isEmpty = BehaviorRelay(value: false)

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    private func setupTableView() {
        
        /// 設置tableFooterView
        tableView.tableFooterView = UIView()
        
        /// 設置代理
        tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
        
        /// 簡單佈局
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalTo(self.view)
        }
        
        /// 設置頭部刷新控件
        tableView.mj_header = MJRefreshNormalHeader()
        /// 設置尾部刷新控件
        tableView.mj_footer = MJRefreshBackNormalFooter()
        
        /// 設置DZNEmptyDataSet的數據源和代理
        tableView.emptyDataSetSource = self
        tableView.emptyDataSetDelegate = self
        
        /// 訂閱點擊了數據爲空,請重試的行爲,裏面沒有用狀態去綁定tableView是由於沒有ViewModel
        emptyDataSetButtonTap.subscribe { [weak self] _ in
            self?.tableView.mj_header?.beginRefreshing()
        }.disposed(by: rx.disposeBag)
        
        /// 數據爲空的訂閱(待用)
        isEmpty.subscribe { event in
            switch event {
            case .next(let noContent):
                break
            default:
                break
            }
        }.disposed(by: rx.disposeBag)
    }

}

//MARK:- UITableViewDelegate
extension BaseTableViewController: UITableViewDelegate {}

//MARK:- DZNEmptyDataSetSource
extension BaseTableViewController: DZNEmptyDataSetSource {

    func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
        return NSAttributedString(string: "暫無數據")
    }

    func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
        return NSAttributedString(string: "嘗試點擊刷新獲取數據")
    }

    func backgroundColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! {
        return .clear
    }

    func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat {
        return -60
    }
}

//MARK:- DZNEmptyDataSetSource
extension BaseTableViewController: DZNEmptyDataSetDelegate {

    func emptyDataSetShouldDisplay(_ scrollView: UIScrollView!) -> Bool {
        return isEmpty.value
    }

    func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool {
        return true
    }
    
    func emptyDataSet(_ scrollView: UIScrollView!, didTap view: UIView!) {
        emptyDataSetButtonTap.onNext(())
    }
}

複製代碼

其實這段DZNEmptyDataSet的代碼,基本上和OC時代寫的代碼沒什麼太多差異,我甚至去看了知名開源App——SwiftHub,想看看大佬有沒有對DZNEmptyDataSet作一層RxSwift的封裝,寫起來更簡單。

結論是沒有!SwiftHub也是在分類裏面寫實現數據源和代理的方式對頁面爲空的狀況作處理。

因而我也在想,費盡精力的去寫第三庫的RxSwift擴展,不如直接用來的省事。

總結

因爲以前寫的積分排行頁面——RxSwiftCoinRankListController是一個獨立的講解頁面,沒有過多去講解BaseViewController。

雖然當時已經使用過了這個基類了,可是筆墨更多的是講解網絡請求和上拉與下拉的操做行爲。

隨着我開始寫首頁的HomeViewModel,我才意識到我漏掉了這一環。

編寫基類,雖然不是必須的,可是有了基類,可能會讓平時的編碼中更爲輕鬆一點,雖然Swift更偏向面向協議編程,可是面向對象編程已經存在這麼多年了,它也有它的優點,繼承使用的當心慎重,思考是否須要繼承都是思考的結晶。

明日繼續

講完基類控制器,下面該講解首頁的編寫了。

你們加油!

相關文章
相關標籤/搜索