// // EmptyDataSet.swift // EmptyDataSet-Swift // // Created by YZF on 28/6/17. // Copyright © 2017年 Xiaoye. All rights reserved. // import Foundation import UIKit class WeakObjectContainer: NSObject { weak var weakObject: AnyObject? init(with weakObject: Any?) { super.init() self.weakObject = weakObject as AnyObject? } } extension UIScrollView: UIGestureRecognizerDelegate { struct Key { static var kEmptyDataSetSource = "emptyDataSetSource" static var kEmptyDataSetDelegate = "emptyDataSetDelegate" static var kEmptyDataSetView = "emptyDataSetView" static var kConfigureEmptyDataSetView = "configureEmptyDataSetView" } private var configureEmptyDataSetView: ((EmptyDataSetView) -> Void)? { get { return objc_getAssociatedObject(self, &Key.kConfigureEmptyDataSetView) as? (EmptyDataSetView) -> Void } set { objc_setAssociatedObject(self, &Key.kConfigureEmptyDataSetView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) UIScrollView.swizzleReloadData if self is UITableView { UIScrollView.swizzleEndUpdates } } } //MARK: - Public Property public var emptyDataSetSource: EmptyDataSetSource? { get { let container = objc_getAssociatedObject(self, &Key.kEmptyDataSetSource) as? WeakObjectContainer return container?.weakObject as? EmptyDataSetSource } set { if newValue == nil { self.invalidate() } objc_setAssociatedObject(self, &Key.kEmptyDataSetSource, WeakObjectContainer(with: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) UIScrollView.swizzleReloadData if self is UITableView { UIScrollView.swizzleEndUpdates } } } public var emptyDataSetDelegate: EmptyDataSetDelegate? { get { let container = objc_getAssociatedObject(self, &Key.kEmptyDataSetDelegate) as? WeakObjectContainer return container?.weakObject as? EmptyDataSetDelegate } set { if newValue == nil { self.invalidate() } objc_setAssociatedObject(self, &Key.kEmptyDataSetDelegate, WeakObjectContainer(with: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } public var isEmptyDataSetVisible: Bool { if let view = objc_getAssociatedObject(self, &Key.kEmptyDataSetView) as? EmptyDataSetView { return !view.isHidden } return false } //MARK: - privateProperty public func emptyDataSetView(_ closure: @escaping (EmptyDataSetView) -> Void) { configureEmptyDataSetView = closure } private var emptyDataSetView: EmptyDataSetView? { get { if let view = objc_getAssociatedObject(self, &Key.kEmptyDataSetView) as? EmptyDataSetView { return view } else { let view = EmptyDataSetView.init(frame: frame) view.autoresizingMask = [.flexibleHeight, .flexibleWidth] view.isHidden = true let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(didTapContentView(_:))) tapGesture.delegate = self view.addGestureRecognizer(tapGesture) view.button.addTarget(self, action: #selector(didTapDataButton(_:)), for: .touchUpInside) objc_setAssociatedObject(self, &Key.kEmptyDataSetView, view, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return view } } set { objc_setAssociatedObject(self, &Key.kEmptyDataSetView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } internal var itemsCount: Int { var items = 0 // UITableView support if let tableView = self as? UITableView { var sections = 1 if let dataSource = tableView.dataSource { if dataSource.responds(to: #selector(UITableViewDataSource.numberOfSections(in:))) { sections = dataSource.numberOfSections!(in: tableView) } if dataSource.responds(to: #selector(UITableViewDataSource.tableView(_:numberOfRowsInSection:))) { for i in 0 ..< sections { items += dataSource.tableView(tableView, numberOfRowsInSection: i) } } } } else if let collectionView = self as? UICollectionView { var sections = 1 if let dataSource = collectionView.dataSource { if dataSource.responds(to: #selector(UICollectionViewDataSource.numberOfSections(in:))) { sections = dataSource.numberOfSections!(in: collectionView) } if dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:numberOfItemsInSection:))) { for i in 0 ..< sections { items += dataSource.collectionView(collectionView, numberOfItemsInSection: i) } } } } return items } //MARK: - Data Source Getters private var titleLabelString: NSAttributedString? { return emptyDataSetSource?.title(forEmptyDataSet: self) } private var detailLabelString: NSAttributedString? { return emptyDataSetSource?.description(forEmptyDataSet: self) } private var image: UIImage? { return emptyDataSetSource?.image(forEmptyDataSet: self) } private var imageAnimation: CAAnimation? { return emptyDataSetSource?.imageAnimation(forEmptyDataSet: self) } private var imageTintColor: UIColor? { return emptyDataSetSource?.imageTintColor(forEmptyDataSet: self) } private func buttonTitle(for state: UIControl.State) -> String? { return emptyDataSetSource?.buttonTitle(forEmptyDataSet: self, for: state) } private func buttonAttributedTitle(for state: UIControl.State) -> NSAttributedString? { return emptyDataSetSource?.buttonAttributedTitle(forEmptyDataSet: self, for: state) } private func buttonImage(for state: UIControl.State) -> UIImage? { return emptyDataSetSource?.buttonImage(forEmptyDataSet: self, for: state) } private func buttonBackgroundImage(for state: UIControl.State) -> UIImage? { return emptyDataSetSource?.buttonBackgroundImage(forEmptyDataSet: self, for: state) } private var dataSetBackgroundColor: UIColor? { return emptyDataSetSource?.backgroundColor(forEmptyDataSet: self) } private var customView: UIView? { return emptyDataSetSource?.customView(forEmptyDataSet: self) } private var verticalOffset: CGFloat { return emptyDataSetSource?.verticalOffset(forEmptyDataSet: self) ?? 0.0 } private var verticalSpace: CGFloat { return emptyDataSetSource?.spaceHeight(forEmptyDataSet: self) ?? 0.0 } //MARK: - Delegate Getters & Events (Private) private var shouldFadeIn: Bool { return emptyDataSetDelegate?.emptyDataSetShouldFadeIn(self) ?? true } private var shouldDisplay: Bool { return emptyDataSetDelegate?.emptyDataSetShouldDisplay(self) ?? true } private var shouldBeForcedToDisplay: Bool { return emptyDataSetDelegate?.emptyDataSetShouldBeForcedToDisplay(self) ?? false } private var isTouchAllowed: Bool { return emptyDataSetDelegate?.emptyDataSetShouldAllowTouch(self) ?? true } private var isScrollAllowed: Bool { return emptyDataSetDelegate?.emptyDataSetShouldAllowScroll(self) ?? false } private var isImageViewAnimateAllowed: Bool { return emptyDataSetDelegate?.emptyDataSetShouldAnimateImageView(self) ?? true } private func willAppear() { emptyDataSetDelegate?.emptyDataSetWillAppear(self) emptyDataSetView?.willAppearHandle?() } private func didAppear() { emptyDataSetDelegate?.emptyDataSetDidAppear(self) emptyDataSetView?.didAppearHandle?() } private func willDisappear() { emptyDataSetDelegate?.emptyDataSetWillDisappear(self) emptyDataSetView?.willDisappearHandle?() } private func didDisappear() { emptyDataSetDelegate?.emptyDataSetDidDisappear(self) emptyDataSetView?.didDisappearHandle?() } @objc private func didTapContentView(_ sender: UITapGestureRecognizer) { guard let view = sender.view else { return } emptyDataSetDelegate?.emptyDataSet(self, didTapView: view) emptyDataSetView?.didTapContentViewHandle?() } @objc private func didTapDataButton(_ sender: UIButton) { emptyDataSetDelegate?.emptyDataSet(self, didTapButton: sender) emptyDataSetView?.didTapDataButtonHandle?() } //MARK: - Reload APIs (Public) public func reloadEmptyDataSet() { guard (emptyDataSetSource != nil || configureEmptyDataSetView != nil) else { return } if (shouldDisplay && itemsCount == 0) || shouldBeForcedToDisplay { // Notifies that the empty dataset view will appear willAppear() if let view = emptyDataSetView { // Configure empty dataset fade in display view.fadeInOnDisplay = shouldFadeIn if view.superview == nil { // Send the view all the way to the back, in case a header and/or footer is present, as well as for sectionHeaders or any other content if (self is UITableView) || (self is UICollectionView) || (subviews.count > 1) { insertSubview(view, at: 0) } else { addSubview(view) } } // Removing view resetting the view and its constraints it very important to guarantee a good state // If a non-nil custom view is available, let's configure it instead view.prepareForReuse() if let customView = self.customView { view.customView = customView } else { // Get the data from the data source let renderingMode: UIImage.RenderingMode = imageTintColor != nil ? .alwaysTemplate : .alwaysOriginal view.verticalSpace = verticalSpace // Configure Image if let image = image { view.imageView.image = image.withRenderingMode(renderingMode) // emptyDataSetView.imageView.mj_size.width = image.size.width // emptyDataSetView.imageView.mj_size.height = image.size.height emptyDataSetView?.imageView.layoutChain.size(image.size) if let imageTintColor = imageTintColor { view.imageView.tintColor = imageTintColor } } // Configure title label if let titleLabelString = titleLabelString { view.titleLabel.attributedText = titleLabelString } // Configure detail label if let detailLabelString = detailLabelString { view.detailLabel.attributedText = detailLabelString } // Configure button if let buttonImage = buttonImage(for: .normal) { view.button.setImage(buttonImage, for: .normal) view.button.setImage(self.buttonImage(for: .highlighted), for: .highlighted) } else if let buttonAttributedTitle = buttonAttributedTitle(for: .normal) { view.button.setAttributedTitle(buttonAttributedTitle, for: .normal) view.button.setAttributedTitle(self.buttonAttributedTitle(for: .highlighted), for: .highlighted) view.button.setBackgroundImage(self.buttonBackgroundImage(for: .normal), for: .normal) view.button.setBackgroundImage(self.buttonBackgroundImage(for: .highlighted), for: .highlighted) } else if let buttonTitle = buttonTitle(for: .normal) { view.button.setTitle(buttonTitle, for: .normal) view.button.setTitle(self.buttonTitle(for: .highlighted), for: .highlighted) view.button.setBackgroundImage(self.buttonBackgroundImage(for: .normal), for: .normal) view.button.setBackgroundImage(self.buttonBackgroundImage(for: .highlighted), for: .highlighted) } } // Configure offset view.verticalOffset = verticalOffset // Configure the empty dataset view view.backgroundColor = dataSetBackgroundColor view.isHidden = false view.clipsToBounds = true // Configure empty dataset userInteraction permission view.isUserInteractionEnabled = isTouchAllowed // Configure scroll permission self.isScrollEnabled = isScrollAllowed // Configure image view animation if self.isImageViewAnimateAllowed { if let animation = imageAnimation { view.imageView.layer.add(animation, forKey: nil) } } else { view.imageView.layer.removeAllAnimations() } if let config = configureEmptyDataSetView { config(view) } view.setupViews() // view.setupConstraints() // view.layoutIfNeeded() } // Notifies that the empty dataset view did appear didAppear() } else if isEmptyDataSetVisible { invalidate() } } private func invalidate() { willDisappear() if let view = emptyDataSetView { view.prepareForReuse() view.isHidden = true // view.removeFromSuperview() // emptyDataSetView = nil } self.isScrollEnabled = true didDisappear() } //MARK: - Method Swizzling @objc private func tableViewSwizzledReloadData() { tableViewSwizzledReloadData() reloadEmptyDataSet() } @objc private func tableViewSwizzledEndUpdates() { tableViewSwizzledEndUpdates() reloadEmptyDataSet() } @objc private func collectionViewSwizzledReloadData() { collectionViewSwizzledReloadData() reloadEmptyDataSet() } private class func swizzleMethod(for aClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) { let originalMethod = class_getInstanceMethod(aClass, originalSelector) let swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector) let didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!)) if didAddMethod { class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!)) } else { method_exchangeImplementations(originalMethod!, swizzledMethod!) } } private static let swizzleReloadData: () = { // let tableViewOriginalSelector = #selector(UITableView.reloadData) // let tableViewSwizzledSelector = #selector(UIScrollView.tableViewSwizzledReloadData) // // swizzleMethod(for: UITableView.self, originalSelector: tableViewOriginalSelector, swizzledSelector: tableViewSwizzledSelector) // // let collectionViewOriginalSelector = #selector(UICollectionView.reloadData) // let collectionViewSwizzledSelector = #selector(UIScrollView.collectionViewSwizzledReloadData) // // swizzleMethod(for: UICollectionView.self, originalSelector: collectionViewOriginalSelector, swizzledSelector: collectionViewSwizzledSelector) }() private static let swizzleEndUpdates: () = { let originalSelector = #selector(UITableView.endUpdates) let swizzledSelector = #selector(UIScrollView.tableViewSwizzledEndUpdates) swizzleMethod(for: UITableView.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector) }() }