Swift -《从0到1 - 6》:创建BaseViewController和BaseTableViewController基类

龚鸿雪
2023-12-01

项目中通常要创建一些基类,方便做一些统一配置或者版本适配,业务控制器可继承这些基类,继承基类的方法和配置

GitHub Demo 地址

1、BaseViewController

BaseViewController只做了版本适配、背景色设置,导航条标题和item设置和点击事件处理,在OC中习惯了block处理点击回调,在swift延续了这种习惯,使用闭包实现了点击回调

//
//  JhBaseViewController.swift
//  JhSwiftDemo
//
//  Created by Jh on 2021/12/28.
//
import UIKit
import Foundation

/// 上下拉刷新状态
enum JhRefreshType : Int {
    /// 状态0 -  默认状态
    case JhRefreshTypeNone = 0
    /// 状态1 - 上拉刷新
    case JhRefreshTypeHeader
    /// 状态2 - 下拉刷新
    case JhRefreshTypeFooter
}

class JhBaseViewController: UIViewController {
    
    deinit {
        print(" JhBaseViewController - dealloc ")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = BaseBgWhiteColor
        
        configIOS11()
        configIOS15()
    }
    
    /// 标题
    var Jh_navTitle :String? {
        didSet {
            self.navigationItem.title = Jh_navTitle
        }
    }
    /// 导航栏左侧标题
    var Jh_navLeftTitle :String? {
        didSet {
            let item =  UIBarButtonItem.Jh_textItem(title: Jh_navLeftTitle ?? "", titleColor: UIColor.white, target: self, action: #selector(ClickLeftItem))
            self.navigationItem.leftBarButtonItem = item
        }
    }
    /// 导航栏左侧img
    var Jh_navLeftImage :String? {
        didSet {
            let item =  UIBarButtonItem.Jh_imageItem(imageName: Jh_navLeftImage ?? "", target: self, action: #selector(ClickLeftItem))
            self.navigationItem.leftBarButtonItem = item
        }
    }
    /// 导航栏右侧标题
    var Jh_navRightTitle :String? {
        didSet {
            let item =  UIBarButtonItem.Jh_textItem(title: Jh_navRightTitle ?? "", titleColor: UIColor.white, target: self, action: #selector(ClickRightItem))
            self.navigationItem.rightBarButtonItem = item
        }
    }
    /// 导航栏右侧img
    var Jh_navRightImage :String? {
        didSet {
            let item =  UIBarButtonItem.Jh_imageItem(imageName: Jh_navRightImage ?? "", target: self, action: #selector(ClickRightItem))
            self.navigationItem.rightBarButtonItem = item
        }
    }
    
    /// 点击导航栏左侧item Block
    var JhClickNavLeftItemBlock:(() -> (Void))?
    /// 点击导航栏右侧item Block
    var JhClickNavRightItemBlock:(() -> (Void))?
    
    @objc func ClickLeftItem() {
        self.JhClickNavLeftItemBlock?()
    }
    
    @objc func ClickRightItem() {
        self.JhClickNavRightItemBlock?()
    }
    
    // MARK: - iOS 适配
    
    func configIOS11() {
        /// 适配 iOS 11.0 ,iOS11以后,控制器的automaticallyAdjustsScrollViewInsets已经废弃,所以默认就会是YES
        /// iOS 11新增:adjustContentInset 和 contentInsetAdjustmentBehavior 来处理滚动区域
        if #available(iOS 11.0, *) {
            UITableView.appearance().estimatedRowHeight = 0
            UITableView.appearance().estimatedSectionHeaderHeight = 0
            UITableView.appearance().estimatedSectionFooterHeight = 0
            // 防止列表/页面偏移
            UIScrollView.appearance().contentInsetAdjustmentBehavior = .never
            UITableView.appearance().contentInsetAdjustmentBehavior = .never
            UICollectionView.appearance().contentInsetAdjustmentBehavior = .never
        } else {
            self.automaticallyAdjustsScrollViewInsets = false
        }
    }
    
    func configIOS15() {
        // 适配iOS15,tableView的section设置
        // iOS15中,tableView会给每一个section的顶部(header以上)再加上一个22像素的高度,形成一个section和section之间的间距
        // 新增的sectionHeaderTopPadding会使表头新增一段间隙,默认为UITableViewAutomaticDimension
        if #available(iOS 15.0, *) {
            UITableView.appearance().sectionHeaderTopPadding = 0
        }
    }
}

使用

 self.Jh_navTitle = "标题";
 Jh_navRightTitle = "文字"
 JhClickNavRightItemBlock = {
     JhLog("点击导航条右侧item")
 }

2、BaseTableView

BaseTableView主要实现了空数据处理(DZNEmptyDataSet实现)和版本适配

//
//  JhBaseTableView.swift
//  JhSwiftDemo
//
//  Created by Jh on 2022/1/5.
//
import UIKit


enum JhEmptyDataViewState : Int {
    /// 状态0 - 暂无数据
    case JhStateNoData = 0
    /// 状态1 -网络请求错误,(网络不可用,请检查网络设置)
    case JhStateNetWorkError
    /// 状态2 - 重新加载
    case JhStateLoading
}

private let kNoDataStr = "暂无数据"
private let kNetWorkErrorStr = "网络不给力,点击重新加载"
private let kNoDataViewOffsetHeight = -(kScreenHeight*0.5 - kNavHeight - 50.0)

class JhBaseTableView: UITableView,DZNEmptyDataSetSource, DZNEmptyDataSetDelegate {
    
    required init?(coder: NSCoder) {
        super.init(coder:coder)
        
        initData()
    }
    
    override init(frame: CGRect, style: UITableView.Style) {
        super.init(frame:frame,style:style)
        
        initData()
    }
    
    /// 点击重新加载按钮 的Block
    var JhClickEmptyViewBlock:(() -> (Void))?
    
    /// 空数据文字
    var Jh_title: String = kNoDataStr
    /// 按钮文字
    var Jh_buttonTitle: String = kNetWorkErrorStr
    
    /// 显示空数据占位图
    /// - Parameter emptyState: 类型
    func JhShowEmptyDataViewWithType(_ emptyState:JhEmptyDataViewState) {
        self.emptyState = emptyState
        configNoDataPicture()
    }
    
    private var emptyState: JhEmptyDataViewState = .JhStateNoData
    
    private func initData() {
        self.backgroundColor = BaseBgWhiteColor
        configIOS11()
    }
    
    // MARK: - iOS 适配
    private func configIOS11() {
        /// 适配 iOS 11.0 ,iOS11以后,控制器的automaticallyAdjustsScrollViewInsets已经废弃,所以默认就会是YES
        /// iOS 11新增:adjustContentInset 和 contentInsetAdjustmentBehavior 来处理滚动区域
        if #available(iOS 11.0, *) {
            self.estimatedRowHeight = 0
            self.estimatedSectionHeaderHeight = 0
            self.estimatedSectionFooterHeight = 0
            // 防止列表/页面偏移
            self.contentInsetAdjustmentBehavior = .never
        }
    }
    
    // MARK: - 设置空数据展示图
    private func configNoDataPicture() {
        self.emptyDataSetSource = self
        self.emptyDataSetDelegate = self
        self.tableFooterView = UIView()
        self.reloadEmptyDataSet()
    }
    
    // MARK: - 防止 刷新后DZNEmptyDataSetView 向上偏移一段距离(空数据展示丢失的问题)
    func emptyDataSetWillAppear(_ scrollView: UIScrollView!) {
        scrollView.contentOffset = CGPoint.zero
    }
    
    // MARK: - 空白界面的标题
    func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
        var title = ""
        if (emptyState == .JhStateNoData) {
            title = kNoDataStr;
        } else if (emptyState == .JhStateNetWorkError) {
            title = kNetWorkErrorStr;
        } else if (emptyState == .JhStateLoading) {
            title = ""
        }
        
        let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: CGFloat(15.0)), NSAttributedString.Key.foregroundColor: BaseEmptyDataTextColor]
        return NSAttributedString(string: title, attributes: attributes)
    }
    
    // MARK: - 调整内容视图的垂直对齐(垂直偏移量)方式
    func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat {
        // 距离顶部距离
        if (emptyState == .JhStateNoData) {
            return kNoDataViewOffsetHeight - 20
        }
        return kNoDataViewOffsetHeight
    }
    
    //    // MARK: - 组件间的空隙 默认11
    //    func spaceHeight(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat {
    //        return 20
    //    }
    
    // MARK: - 空白页上view点击事件
    func emptyDataSet(_ scrollView: UIScrollView!, didTap view: UIView!) {
        JhLog(" JhBaseTableView - view点击事件 ")
    }
    
    // MARK: - 是否允许滚动,默认NO
    func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool {
        return true
    }
    
    // MARK: - 空白页上Btn点击事件
    func emptyDataSet(_ scrollView: UIScrollView!, didTap button: UIButton!) {
        if (emptyState == .JhStateNetWorkError) {
            JhLog(" JhBaseTableView - 点击了空白页上按钮 ")
            emptyState = .JhStateLoading
            self.reloadEmptyDataSet()
            self.JhClickEmptyViewBlock?()
        }
    }
    
    // MARK: - 设置按钮图片(这张图片是带边框带重新加载文字的图片)
    func buttonImage(forEmptyDataSet scrollView: UIScrollView!, for state: UIControl.State) -> UIImage! {
        var image = UIImage(named: "empty")
        if (emptyState == .JhStateNetWorkError) {
            image = UIImage(named:"JhEmptyDataView.bundle/NullData_reloadData")
        } else {
            image = UIImage(named: "empty")
        }
        return image
    }
}

3、BaseTableViewController

BaseTableViewController主要针对简单的分页列表做的封装,内部还封装了一个网络请求方法,业务类可直接继承基类,自定义业务相关cell和model,快速实现分页列表

//
//  JhCustumCellTableViewController.swift
//  JhSwiftDemo
//
//  Created by Jh on 2022/1/5.
//
import UIKit
import Moya
import SwiftyJSON

private let CellID = "CellID"

class JhCustumCellTableViewController: JhBaseViewController, UITableViewDelegate, UITableViewDataSource {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configTableView()
    }
    
    private func configTableView() {
        self.view.addSubview(self.Jh_tableView)
    }
    
    /// 自定义Cell Block
    var JhCustumCellBlock: ((_ indexPath : IndexPath, _ custumCell : Any ) ->())?
    /// 点击Cell Block
    var JhClickCustumCellBlock: ((_ indexPath : IndexPath) ->())?
    /// Cell高度 Block
    var JhCellHeightBlock: ((_ indexPath : IndexPath, _ cellHeight : CGFloat ) ->())?
    
    /// 自定义的CellName
    var Jh_cellName: String? {
        didSet {
            // Nib注册Cell
            Jh_tableView.register(UINib(nibName: Jh_cellName ?? "JhBaseTableViewCell", bundle: nil), forCellReuseIdentifier: CellID)
        }
    }
    
    /// 数据源
    var Jh_modelArr: Array<Any> {
        get {
            return dataArr
        }
        set(newValue) {
            dataArr = newValue
            // 显示空数据占位图
            if (dataArr.count == 0) {
                Jh_tableView.JhShowEmptyDataViewWithType(.JhStateNoData)
            }
            Jh_tableView.reloadData()
        }
    }
    
    /// Cell高度
    var Jh_cellHeight: CGFloat = 44.0 {
        didSet {
            self.cellHeight = Jh_cellHeight
        }
    }
    
    /// 隐藏分割线
    var Jh_hiddenLine: Bool = false {
        didSet {
            // Nib注册Cell
            Jh_tableView.separatorStyle = Jh_hiddenLine ? .none :.singleLine
        }
    }
    
    //*****************是否开启头部刷新和脚部刷新 子类可在ViewDidLoad方法设置开启与否 默认都不开启******************************//
    
    /// 立即开始头部刷新,默认值==true  (如果不需要立即刷新,需要在设置头部尾部刷新前设置此属性)
    var Jh_isStartRefresh: Bool = true {
        didSet {
            // Nib注册Cell
            Jh_tableView.separatorStyle = Jh_hiddenLine ? .none :.singleLine
        }
    }
    /// 开启头部和尾部刷新
    var Jh_isOpenHeaderAndFooterRefresh: Bool = false {
        didSet {
            if (Jh_isOpenHeaderAndFooterRefresh == true) {
                Jh_tableView.mj_header = JhRefreshHeader(refreshingTarget: self, refreshingAction: #selector(JhHeaderRefresh))
                if (Jh_isStartRefresh == true) {
                    Jh_tableView.mj_header?.beginRefreshing()
                }
                Jh_tableView.mj_footer = JhRefreshFooter(refreshingTarget: self, refreshingAction: #selector(JhFooterRefresh))
            } else {
                JhLog(" 父类 - 不开启头部尾部刷新");
            }
        }
    }
    
    //*****************是否开启头部刷新和脚部刷新 子类可在ViewDidLoad方法设置开启与否 默认都不开启******************************//
    
    // MARK: - TableViewDataSource
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // 第一次进来或者每次reloadData否会调一次该方法,在此控制footer是否隐藏
        if (Jh_isOpenHeaderAndFooterRefresh == true) {
            Jh_tableView.mj_footer?.isHidden = dataArr.count == 0
        }
        return dataArr.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CellID, for: indexPath)
        JhCustumCellBlock?(indexPath,cell)
        return cell
    }
    
    // MARK: - UITableViewDelegate
    
    // MARK: - 设置cell高度
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        JhCellHeightBlock?(indexPath,cellHeight)
        return cellHeight
    }
    
    // MARK: - 选中某行的点击操作
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true) // 取消选中
        
        JhClickCustumCellBlock?(indexPath)
    }
    
    // MARK: - ***** 头部刷新and尾部刷新
    @objc func JhHeaderRefresh() {
        JhLog(" 父类 - JhHeaderRefresh ")
    }
    
    @objc func JhFooterRefresh() {
        JhLog(" 父类 - JhFooterRefresh ")
    }
    
    /// 显示网络错误站位图 ,无网络 点击重新加载按钮 重新请求数据
    func JhShowNetWorkErrorWithReloadBlock(block:(() ->Void)?) {
        if (dataArr.count == 0) {
            Jh_tableView.JhShowEmptyDataViewWithType(.JhStateNetWorkError)
            Jh_tableView.JhClickEmptyViewBlock = {
                block?()
            }
        }
    }
    
    lazy var Jh_tableView: JhBaseTableView = {
        let tableView = JhBaseTableView(frame: .zero)
        tableView.frame = kTableViewFrame
        //        tableView.separatorStyle = .none // 不显示分割线
        //        tableView.showsVerticalScrollIndicator = false
        //        tableView.scrollEnabled = false // 设置tableview 不能滚动
        tableView.dataSource = self
        tableView.delegate = self
        tableView.backgroundColor = BaseBgWhiteColor
        tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 1))
        tableView.tableFooterView = UIView()
        return tableView
    }()
    
    private lazy var dataArr: Array<Any> = {
        var tempArr = Array<Any>()
        return tempArr
    }()
    
    private var cellHeight: CGFloat = 44.0
    
}

extension JhCustumCellTableViewController {
    /// listCell 发送请求
    func Jh_Request_ListCell<T: TargetType>(_ target: T,_ isLoadMore:Bool = false, success: @escaping((Any) -> Void), failure: ((Int?, String) ->Void)?) {
        // Alamofire + Moya + SwiftyJSON
        JhHttpTool.request(target) {[weak self] json in
            JhAllLog("*******************  自定义cell基类 - \(isLoadMore ? "尾部刷新" : "头部刷新") 请求成功 ******************* res 为:\(JSON(json))")
            
            if (isLoadMore) {
                self?.Jh_tableView.mj_footer?.endRefreshing()
            } else {
                self?.Jh_tableView.mj_header?.endRefreshing()
            }
            
            let res = JSON(json)
            let code = res["code"]
            if (code == 200) {
                let data = res["data"].rawValue
                success(data)
            }
        } failure: {code, msg in
            JhLog("code : \(code!)")
            JhLog("message : \(msg)")
            if (isLoadMore) {
                self.Jh_tableView.mj_footer?.endRefreshing()
            } else {
                self.Jh_tableView.mj_header?.endRefreshing()
                // 显示网络超时占位图 和 点击事件(重新请求)
                self.JhShowNetWorkErrorWithReloadBlock {
                    self.Jh_tableView.mj_header?.beginRefreshing()
                }
            }
            failure?(code, msg)
        }
    }
    
}

使用

//
//  DemoCustomTableVC5.swift
//  JhSwiftDemo
//
//  Created by Jh on 2022/1/4.
//
import UIKit
import SwiftyJSON

class DemoCustomTableVC5: JhCustumCellTableViewController {
    
    private lazy var dataArr: Array<DemoCustomModel2> = {
        var tempArr = Array<DemoCustomModel2>()
        return tempArr
    }()
    private var page:Int = 0
    private var isLoadAll:Bool = false //已经加载全部数据
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configTableView()
        requestData()
    }
    
    func configTableView() {
        Jh_navTitle = "二次封装请求 + MJExtension"
        
        Jh_isOpenHeaderAndFooterRefresh = true
        Jh_cellName = "DemoCustomTableViewCell2"
        
        // 高度设置
        //        Jh_cellHeight = 50
        JhCellHeightBlock = { [weak self] (indexPath,cellHeight) ->() in
            let data = self!.Jh_modelArr[indexPath.row] as! DemoCustomModel2
            self?.Jh_cellHeight = data.cellHeight
        }
        
        JhCustumCellBlock = { [weak self] (indexPath,custumCell) ->() in
            let cell:DemoCustomTableViewCell2 = custumCell as! DemoCustomTableViewCell2
            cell.configCellData(self!.dataArr[indexPath.row])
        }
        
        JhClickCustumCellBlock = { [weak self] (indexPath) ->() in
            JhLog("选中section:\(indexPath.section) ")
            JhLog("选中row:\(indexPath.row) ")
            
            let data = self!.dataArr[indexPath.row]
            JhLog("选中Text : \(data.name2)")
            JhLog("选中ID : \(data.id)")
            
            JhProgressHUD.showText(data.name2)
        }
        
    }
    
    override func JhHeaderRefresh() {
        JhLog(" 子类 - JhHeaderRefresh ")
        requestData()
    }
    
    override func JhFooterRefresh() {
        JhLog(" 子类 - JhFooterRefresh ")
        requestData(true)
    }
    
    func requestData(_ isLoadMore:Bool = false) {
        if (isLoadMore) {
            page = page + 1
        } else {
            page = 0;
        }
        JhLog("page : \(page)")

        Jh_Request_ListCell(API.getPageList(page),isLoadMore) { [weak self] json in
            let data = String(describing: json) != "" ? json : []
            let tempArr = DemoCustomModel2.mj_objectArray(withKeyValuesArray: data)as! Array<DemoCustomModel2>
            if (isLoadMore) {
                self?.dataArr += tempArr
            } else {
                self?.dataArr = tempArr
            }
            self?.Jh_modelArr = self!.dataArr
            self?.isLoadAll = tempArr.count < 15 ? true : false
            self?.Jh_tableView.reloadData()
        } failure: { code, msg in
            self.page = self.page - 1
            self.page = self.page < 0 ? 0 : self.page
        }
        
    }
    
}
 类似资料: