封装TableView无数据占位图
1. 原理
通过runtime黑魔法交换UITableView的reloadData方法和自定义placeHolder_reloadData方法,然后在自定义reloadData方法里检查section数量,再根据section确定每个区的row的数量,只要不为0就要展示noDataPlaceHolder,通过代理获取自定义的占位View或者图片、文字、按钮、点击刷新方法等等。占位图直接赋值给tableView的backgroundView。依赖观察者观察frame的变化,更改占位图的位置,可以随着tableView滚动。
2. 实现步骤 Swift版本和OC版本的原理相同,但是步骤稍有区别
#####1.准备工作:分类里只交换一次,同dispatch_once的效果,这里通过UIApplication的分类里的 next属性,实现类似OC的load方法,这里会通过方法便利加载的类列表,逐个执行自定义方法,这里我用awake() ,这里就可以把awake()看成load()方法进行实现
application分类
extension UIApplication {
private static let runOnce: Void = {
NothingToSeeHere.harmlessFunction()
}()
override open var next: UIResponder? {
UIApplication.runOnce
return super.next
}
}
协议方法
protocol SelfAware: class {
static func awake()
static func swizzlingForClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector)
}
extension SelfAware {
static func swizzlingForClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(forClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)
guard (originalMethod != nil && swizzledMethod != nil) else {
return
}
if class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!)) {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
} else {
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}
}
便利类列表逐个调用awake()的代码
class NothingToSeeHere {
static func harmlessFunction() {
let typeCount = Int(objc_getClassList(nil, 0))
let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
objc_getClassList(autoreleasingTypes, Int32(typeCount))
for index in 0 ..< typeCount {
(types[index] as? SelfAware.Type)?.awake()
}
types.deallocate()
}
}
2.实现
占位图协议
//占位图的协议
@objc protocol PlaceHolderTableViewDelegate :class {
@objc optional func placeHolder_noDataView() -> UIView//完全自定义占位View
@objc optional func placeHolder_noDataViewImage() -> UIImage//更换默认占位Image
@objc optional func placeHolder_noDataViewTitle() -> String//更换默认标题
@objc optional func placeHolder_noDataViewCenterYOffset() -> Float//变更占位竖直方向偏移量
@objc optional func tapForReload()//点击重试 这里点击区域是整个列表
@objc optional func placeHolder_noDataViewBtnImage() -> UIImage//点击按钮的图片 如果传图片了,按钮就不隐藏
@objc optional func tapBtnForOther()//点击按钮 其他行为
}
UITableView的分类
extension UITableView: SelfAware{
//实现自定义reloadData方法和系统reloadData方法的交换
static func awake() {
swizzleMethod
}
private static let swizzleMethod: Void = {
let originalSelector = #selector(reloadData)
let swizzledSelector = #selector(swizzled_reloadData)
swizzlingForClass(UITableView.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector)
}()
//自定义reloadData方法
@objc func swizzled_reloadData() {
swizzled_reloadData()
// print("swizzled_reloadData")
if !self.isInitFinish {
self.isInitFinish = true
return
}
DispatchQueue.main.async {
let numSections = self.numberOfSections
var haveData = false
if numSections > 0 {
for section in 0..<numSections{
if self.numberOfRows(inSection: section) > 0{
haveData = true
break
}
}
}
self.placeHolder_Show(haveData: haveData, netReach: true)
}
}
// 嵌套结构体
private struct AssociatedKeys {
static var isInitFinishKey = "isInitFinishKey"
// static var placeHolderDelegateKey = "placeHolderDelegateKey"
}
//关联对象 是否加载完成数据
var isInitFinish:Bool {
get {
return (objc_getAssociatedObject(self, &AssociatedKeys.isInitFinishKey) as? Bool) ?? false
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.isInitFinishKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
//展示展位图
func placeHolder_Show(haveData:Bool,netReach:Bool) {
//不需要展示展位图
if haveData {
self.backgroundView = nil
return
}
//不需要重建
if self.backgroundView != nil {
return
}
//自定义占位图
if (self.delegate?.conforms(to: PlaceHolderTableViewDelegate.self))! && (self.delegate as? PlaceHolderTableViewDelegate)?.placeHolder_noDataView?() != nil {
self.backgroundView = (self.delegate as? PlaceHolderTableViewDelegate)?.placeHolder_noDataView?()
return
}
//默认占位图配置
var img:UIImage = UIImage(named: "Img_CollectionViewNoDataIcon")!
var title:String = netReach ? "暂无数据" : "无网络连接,请点击重试"
// var titleColor:UIColor = .lightGray
var offSetY:Float = 0
var btnImage:UIImage? = nil
if (self.delegate?.conforms(to: PlaceHolderTableViewDelegate.self))! {
img = ((self.delegate as? PlaceHolderTableViewDelegate)?.placeHolder_noDataViewImage?()) ?? UIImage(named: "Img_CollectionViewNoDataIcon")!
title = (self.delegate as? PlaceHolderTableViewDelegate)?.placeHolder_noDataViewTitle?() ?? (netReach ? "暂无数据" : "无网络连接,请点击重试")
offSetY = ((self.delegate as? PlaceHolderTableViewDelegate)?.placeHolder_noDataViewCenterYOffset?()) ?? 0
btnImage = (self.delegate as? PlaceHolderTableViewDelegate)?.placeHolder_noDataViewBtnImage?()
}
self.backgroundView = self.placeHolder_Default(img: img, title: title,btnImage:btnImage,offSetY: CGFloat(offSetY))
}
func placeHolder_Default(img:UIImage,title:String,btnImage:UIImage?,offSetY:CGFloat) -> UIView {
// 计算位置, 垂直居中, 图片默认中心偏上.
let sW = self.bounds.size.width
let cX = sW / 2.0
let cY = self.bounds.size.height * (1-0.618) + offSetY
let iW = img.size.width
let iH = img.size.height
//图片
let imgView:UIImageView = UIImageView()
imgView.frame = CGRect(x: cX - iW / 2, y: cY - iH / 2, width: iW, height: iH)
imgView.image = img
//文字
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13)
label.textColor = UIColor.hexColor("#333333")
label.text = title
label.textAlignment = .center
label.frame = CGRect(x: 0, y: imgView.frame.size.height+imgView.frame.origin.y+24, width: sW, height: label.font.lineHeight)
//点击按钮 默认隐藏
let btn:UIButton = UIButton()
btn.setNormalCornerRadious(radious: 18)
btn.isHidden = btnImage == nil
if btnImage != nil {
let bW = btnImage?.size.width
let bH = btnImage?.size.height
let bL = bW! / 2.0
btn.frame = CGRect(x: cX-bL, y: label.frame.size.height+label.frame.origin.y+20, width: bW!, height: bH!)
btn.setBackgroundImage(btnImage, for: .normal)
}
btn.setBackgroundImage(btnImage, for: .normal)
btn.addTarget(self, action: #selector(btnClicked), for: .touchUpInside)
//视图
let view:UIView = UIView()
view.addSubview(imgView)
view.addSubview(label)
view.addSubview(btn)
view.addObserver(self, forKeyPath: "frame", options: NSKeyValueObservingOptions.new, context: nil)
let tap = UITapGestureRecognizer.init(target: self, action: #selector(tapClicked))
view.addGestureRecognizer(tap)
return view
}
//观察者
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "frame" {
var frame = (change! as NSDictionary).value(forKey: NSKeyValueChangeKey.newKey.rawValue) as! CGRect
if (frame.origin.y != 0) {
frame.origin.y = 0;
self.backgroundView!.frame = frame;
}
}
}
//点击刷新
@objc func btnClicked(){
if (self.delegate?.conforms(to: PlaceHolderTableViewDelegate.self))! {
(self.delegate as? PlaceHolderTableViewDelegate)?.tapBtnForOther?()
}
}
//按钮点击
@objc func tapClicked(){
if (self.delegate?.conforms(to: PlaceHolderTableViewDelegate.self))! {
(self.delegate as? PlaceHolderTableViewDelegate)?.tapForReload?()
}
}
//移除监听
func removeObserveForBackgroundView() {
self.backgroundView?.removeObserver(self, forKeyPath: "frame")
}
}