手记

Swift总结笔记

封装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")
    }
 
}
0人推荐
随时随地看视频
慕课网APP