项目中通常要创建一些基类,方便做一些统一配置或者版本适配,业务控制器可继承这些基类,继承基类的方法和配置
GitHub Demo 地址
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")
}
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
}
}
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
}
}
}