Repository: teambition/TBEmptyDataSet Branch: master Commit: b1e704d33322 Files: 37 Total size: 109.0 KB Directory structure: gitextract_9vpatnox/ ├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── Example/ │ ├── .swiftlint.yml │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── icon-empty-events.imageset/ │ │ │ └── Contents.json │ │ ├── icon-empty-message.imageset/ │ │ │ └── Contents.json │ │ ├── icon-empty-photos.imageset/ │ │ │ └── Contents.json │ │ └── loading.imageset/ │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── Main.storyboard │ ├── DemoViewController.swift │ ├── EmptyDataDemoCollectionViewController.swift │ ├── EmptyDataDemoTableViewController.swift │ └── Info.plist ├── LICENSE.md ├── README.md ├── TBEmptyDataSet/ │ ├── Constants.swift │ ├── EmptyDataView.swift │ ├── Protocols.swift │ ├── Supporting Files/ │ │ └── Info.plist │ ├── TBEmptyDataSet.swift │ └── WeakObjectContainer.swift ├── TBEmptyDataSet.podspec ├── TBEmptyDataSet.xcodeproj/ │ ├── project.pbxproj │ ├── xcshareddata/ │ │ └── xcschemes/ │ │ └── TBEmptyDataSet.xcscheme │ └── xcuserdata/ │ ├── hongxin.xcuserdatad/ │ │ └── xcschemes/ │ │ └── xcschememanagement.plist │ └── zetasq.xcuserdatad/ │ └── xcschemes/ │ └── xcschememanagement.plist ├── TBEmptyDataSet.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist └── TBEmptyDataSetExample.xcodeproj/ ├── project.pbxproj ├── project.xcworkspace/ │ └── contents.xcworkspacedata └── xcuserdata/ ├── hongxin.xcuserdatad/ │ └── xcschemes/ │ ├── TBEmptyDataSetExample.xcscheme │ └── xcschememanagement.plist └── zetasq.xcuserdatad/ └── xcschemes/ ├── TBEmptyDataSetExample.xcscheme └── xcschememanagement.plist ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ TBEmptyDataSet.xcworkspace/xcuserdata ================================================ FILE: .swift-version ================================================ 5.0 ================================================ FILE: .swiftlint.yml ================================================ disabled_rules: # rule identifiers to exclude from running - line_length - nesting - variable_name_min_length - cyclomatic_complexity - class_delegate_protocol - multiple_closures_with_trailing_closure included: # paths to include during linting. `--path` is ignored if present. takes precendence over `excluded`. excluded: # paths to ignore during linting. overridden by `included`. - Carthage # parameterized rules are first parameterized as a warning level, then error level. line_length: 150 type_name: min_length: 3 # only warning max_length: # warning and error warning: 100 error: 200 function_parameter_count: 10 function_body_length: - 100 - 200 type_body_length: - 300 # warning - 400 # error file_length: - 1000 # warning - 1500 # error variable_name: min_length: 2 max_length: 40 ================================================ FILE: Example/.swiftlint.yml ================================================ disabled_rules: # rule identifiers to exclude from running - line_length - nesting included: # paths to include during linting. `--path` is ignored if present. takes precendence over `excluded`. excluded: # paths to ignore during linting. overridden by `included`. - Carthage # parameterized rules can be customized from this configuration file line_length: 150 # parameterized rules are first parameterized as a warning level, then error level. type_body_length: - 300 # warning - 400 # error ================================================ FILE: Example/AppDelegate.swift ================================================ // // AppDelegate.swift // TBEmptyDataSetExample // // Created by 洪鑫 on 15/11/19. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Example/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "1x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "2x" }, { "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Assets.xcassets/icon-empty-events.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icon-empty-events.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Assets.xcassets/icon-empty-message.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icon-empty-message.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Assets.xcassets/icon-empty-photos.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icon-empty-photos.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Assets.xcassets/loading.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "loading@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "loading.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/DemoViewController.swift ================================================ // // DemoViewController.swift // TBEmptyDataSetExample // // Created by 洪鑫 on 15/11/26. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit public struct EmptyData { static let images = [#imageLiteral(resourceName: "icon-empty-photos"), #imageLiteral(resourceName: "icon-empty-events"), #imageLiteral(resourceName: "icon-empty-message")] static let titles = ["无照片", "无日程", "无新消息"] static let descriptions = ["你可以添加一些照片哦,让生活更精彩!", "暂时没有日程哦,添加一些日程吧!", "没有新消息哦,去找朋友聊聊天吧!"] } class DemoViewController: UITableViewController { // MARK: - Structs fileprivate struct Data { static let examples = ["Empty Photos", "Empty Events", "Empty Message"] static let sectionTitles = ["TableView", "CollectionView"] } fileprivate struct SegueIdentifier { static let showTableView = "ShowEmptyDataDemoTableView" static let showCollectionView = "ShowEmptyDataDemoCollectionView" } fileprivate struct CellIdentifier { static let reuseIdentifier = "Cell" } // MARK: - Properties var selectedIndexPath = IndexPath() // MARK: - View life cycle override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "EmptyDataSet Example" let backButton = UIBarButtonItem() backButton.title = "Back" navigationItem.backBarButtonItem = backButton tableView.tableFooterView = UIView() } // MARK: - Table view data source and delegate override func numberOfSections(in tableView: UITableView) -> Int { return Data.sectionTitles.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return Data.examples.count } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return Data.sectionTitles[section] } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.reuseIdentifier) if cell == nil { cell = UITableViewCell(style: .default, reuseIdentifier: CellIdentifier.reuseIdentifier) } cell!.accessoryType = .disclosureIndicator cell!.textLabel!.text = Data.examples[indexPath.row] return cell! } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { selectedIndexPath = indexPath if indexPath.section == 0 { performSegue(withIdentifier: SegueIdentifier.showTableView, sender: self) } else if indexPath.section == 1 { performSegue(withIdentifier: SegueIdentifier.showCollectionView, sender: self) } tableView.deselectRow(at: indexPath, animated: true) } // MARK: - Navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == SegueIdentifier.showTableView { if let emptyDataDemoTableViewController = segue.destination as? EmptyDataDemoTableViewController { emptyDataDemoTableViewController.indexPath = selectedIndexPath } } else if segue.identifier == SegueIdentifier.showCollectionView { if let emptyDataDemoCollectionViewController = segue.destination as? EmptyDataDemoCollectionViewController { emptyDataDemoCollectionViewController.indexPath = selectedIndexPath } } } } ================================================ FILE: Example/EmptyDataDemoCollectionViewController.swift ================================================ // // EmptyDataDemoCollectionViewController.swift // TBEmptyDataSetExample // // Created by 洪鑫 on 15/11/24. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit import TBEmptyDataSet class EmptyDataDemoCollectionViewController: UICollectionViewController { // MARK: - Structs fileprivate struct CellIdentifier { static let reuseIdentifier = "Cell" } // MARK: - Properties var indexPath = IndexPath() fileprivate var isLoading = false fileprivate var dataCount = 0 // MARK: - View life cycle override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "CollectionView" collectionView?.backgroundColor = .white collectionView?.emptyDataSetDataSource = self collectionView?.emptyDataSetDelegate = self if indexPath.row != 0 { loadData(self) } } // MARK: - Helper func loadData(_ sender: Any) { isLoading = true let delayTime = DispatchTime.now() + Double(Int64(2 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) DispatchQueue.main.asyncAfter(deadline: delayTime) { () -> Void in self.dataCount = 4 self.isLoading = false self.collectionView?.reloadData() } } // MARK: - Collection view data source override func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return dataCount } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier.reuseIdentifier, for: indexPath) let maskLayer = CAShapeLayer() let maskRect = cell.bounds maskLayer.frame = maskRect let cornerRadii = CGSize(width: 5, height: 5) let maskPath = UIBezierPath(roundedRect: maskRect, byRoundingCorners: .allCorners, cornerRadii: cornerRadii) maskLayer.path = maskPath.cgPath cell.layer.mask = maskLayer return cell } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { self.dataCount -= 1 collectionView.performBatchUpdates({ self.collectionView?.deleteItems(at: [indexPath]) }) { (_) in } } } extension EmptyDataDemoCollectionViewController: TBEmptyDataSetDataSource, TBEmptyDataSetDelegate { // MARK: - TBEmptyDataSet data source func imageForEmptyDataSet(in scrollView: UIScrollView) -> UIImage? { return EmptyData.images[indexPath.row] } func titleForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { let title = EmptyData.titles[indexPath.row] var attributes: [NSAttributedString.Key: Any]? if indexPath.row == 1 { attributes = [.font: UIFont.systemFont(ofSize: 22), .foregroundColor: UIColor.gray] } else if indexPath.row == 2 { attributes = [.font: UIFont.systemFont(ofSize: 24), .foregroundColor: UIColor.gray] } return NSAttributedString(string: title, attributes: attributes) } func descriptionForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { let description = EmptyData.descriptions[indexPath.row] var attributes: [NSAttributedString.Key: Any]? if indexPath.row == 1 { attributes = [.font: UIFont.systemFont(ofSize: 17), .foregroundColor: UIColor(red: 3 / 255, green: 169 / 255, blue: 244 / 255, alpha: 1)] } else if indexPath.row == 2 { attributes = [.font: UIFont.systemFont(ofSize: 18), .foregroundColor: UIColor.purple] } return NSAttributedString(string: description, attributes: attributes) } func backgroundColorForEmptyDataSet(in scrollView: UIScrollView) -> UIColor? { return UIColor(white: 0.95, alpha: 1) } func verticalOffsetForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { if let navigationBar = navigationController?.navigationBar { return -navigationBar.frame.height * 0.75 } return 0 } func customViewForEmptyDataSet(in scrollView: UIScrollView) -> UIView? { let loadingView: UIView = { if indexPath.row == 2 { return ExampleCustomView(frame: CGRect.zero) } let loadingImageView = UIImageView(image: #imageLiteral(resourceName: "loading")) let view = UIView(frame: loadingImageView.frame) view.addSubview(loadingImageView) let animation: CABasicAnimation = { let animation = CABasicAnimation(keyPath: "transform") animation.fromValue = NSValue(caTransform3D: CATransform3DIdentity) animation.toValue = NSValue(caTransform3D: CATransform3DMakeRotation(.pi / 2, 0, 0, 1)) animation.duration = 0.3 animation.isCumulative = true animation.repeatCount = .greatestFiniteMagnitude return animation }() loadingImageView.layer.add(animation, forKey: "loading") return view }() if isLoading { return loadingView } else { return nil } } // MARK: - TBEmptyDataSet delegate func emptyDataSetShouldDisplay(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetTapEnabled(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetScrollEnabled(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetDidTapEmptyView(in scrollView: UIScrollView) { let alert = UIAlertController(title: nil, message: "Did Tap EmptyDataView!", preferredStyle: .alert) let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(cancelAction) present(alert, animated: true, completion: nil) } func emptyDataSetWillAppear(in scrollView: UIScrollView) { print("EmptyDataSet Will Appear!") } func emptyDataSetDidAppear(in scrollView: UIScrollView) { print("EmptyDataSet Did Appear!") } func emptyDataSetWillDisappear(in scrollView: UIScrollView) { print("EmptyDataSet Will Disappear!") } func emptyDataSetDidDisappear(in scrollView: UIScrollView) { print("EmptyDataSet Did Disappear!") } } extension EmptyDataDemoCollectionViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 150, height: 90) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 20 } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return 20 } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) collectionViewLayout.invalidateLayout() coordinator.animate(alongsideTransition: { (_) -> Void in }) { (_) -> Void in } } } class ExampleCustomView: UIView { fileprivate lazy var contentLabel: UILabel = { let contentLabel = UILabel() contentLabel.numberOfLines = 0 contentLabel.textColor = .white contentLabel.translatesAutoresizingMaskIntoConstraints = false return contentLabel }() override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) commonInit() } fileprivate func commonInit() { backgroundColor = .lightGray layer.cornerRadius = 6 let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false activityIndicatorView.startAnimating() contentLabel.text = "Loading... Please wait a moment...\n\n(This is a custom empty data view, which is using pure AutoLayout)" addSubview(activityIndicatorView) addSubview(contentLabel) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-15-[activityIndicatorView]-20-[contentLabel]-15-|", options: [], metrics: nil, views: ["activityIndicatorView": activityIndicatorView, "contentLabel": contentLabel])) addConstraint(NSLayoutConstraint(item: activityIndicatorView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-15-[contentLabel]-15-|", options: [], metrics: nil, views: ["contentLabel": contentLabel])) translatesAutoresizingMaskIntoConstraints = false } } ================================================ FILE: Example/EmptyDataDemoTableViewController.swift ================================================ // // EmptyDataDemoTableViewController.swift // TBEmptyDataSetExample // // Created by 洪鑫 on 15/11/24. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit import TBEmptyDataSet class EmptyDataDemoTableViewController: UITableViewController { // MARK: - Structs fileprivate struct CellIdentifier { static let reuseIdentifier = "Cell" } // MARK: - Properties var indexPath = IndexPath() fileprivate var isLoading = false fileprivate var dataCount = 0 // MARK: - View life cycle override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "TableView" tableView.tableFooterView = UIView() refreshControl?.addTarget(self, action: #selector(fetchData(_:)), for: .valueChanged) tableView.emptyDataSetDataSource = self tableView.emptyDataSetDelegate = self if indexPath.row != 0 { loadData(self) } } // MARK: - Helper @objc func fetchData(_ sender: Any) { let delayTime = DispatchTime.now() + Double(Int64(1.5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) DispatchQueue.main.asyncAfter(deadline: delayTime) { () -> Void in self.dataCount = 7 self.tableView.reloadData() self.refreshControl?.endRefreshing() } } func loadData(_ sender: Any) { isLoading = true let delayTime = DispatchTime.now() + Double(Int64(1.75 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) DispatchQueue.main.asyncAfter(deadline: delayTime) { () -> Void in self.isLoading = false self.tableView.reloadData() } } // MARK: - Table view data source and delegate override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataCount } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.reuseIdentifier) if cell == nil { cell = UITableViewCell(style: .value1, reuseIdentifier: CellIdentifier.reuseIdentifier) } cell!.textLabel?.text = "Cell" cell!.detailTextLabel?.text = "Click to delete" return cell! } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) tableView.beginUpdates() dataCount -= 1 tableView.deleteRows(at: [indexPath], with: .fade) tableView.endUpdates() } } extension EmptyDataDemoTableViewController: TBEmptyDataSetDataSource, TBEmptyDataSetDelegate { // MARK: - TBEmptyDataSet data source func imageForEmptyDataSet(in scrollView: UIScrollView) -> UIImage? { return EmptyData.images[indexPath.row] } func titleForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { let title = EmptyData.titles[indexPath.row] var attributes: [NSAttributedString.Key: Any]? if indexPath.row == 1 { attributes = [.font: UIFont.systemFont(ofSize: 22), .foregroundColor: UIColor.gray] } else if indexPath.row == 2 { attributes = [.font: UIFont.systemFont(ofSize: 24), .foregroundColor: UIColor.gray] } return NSAttributedString(string: title, attributes: attributes) } func descriptionForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { let description = EmptyData.descriptions[indexPath.row] var attributes: [NSAttributedString.Key: Any]? if indexPath.row == 1 { attributes = [.font: UIFont.systemFont(ofSize: 17), .foregroundColor: UIColor(red: 3 / 255, green: 169 / 255, blue: 244 / 255, alpha: 1)] } else if indexPath.row == 2 { attributes = [.font: UIFont.systemFont(ofSize: 18), .foregroundColor: UIColor.purple] } return NSAttributedString(string: description, attributes: attributes) } func verticalOffsetForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { if let navigationBar = navigationController?.navigationBar { return -navigationBar.frame.height * 0.75 } return 0 } func verticalSpacesForEmptyDataSet(in scrollView: UIScrollView) -> [CGFloat] { return [25, 8] } func customViewForEmptyDataSet(in scrollView: UIScrollView) -> UIView? { if isLoading { let activityIndicator = UIActivityIndicatorView(style: .gray) activityIndicator.startAnimating() return activityIndicator } return nil } // MARK: - TBEmptyDataSet delegate func emptyDataSetShouldDisplay(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetTapEnabled(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetScrollEnabled(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetDidTapEmptyView(in scrollView: UIScrollView) { let alert = UIAlertController(title: nil, message: "Did Tap EmptyDataView!", preferredStyle: .alert) let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(cancelAction) present(alert, animated: true, completion: nil) } func emptyDataSetWillAppear(in scrollView: UIScrollView) { print("EmptyDataSet Will Appear!") } func emptyDataSetDidAppear(in scrollView: UIScrollView) { print("EmptyDataSet Did Appear!") } func emptyDataSetWillDisappear(in scrollView: UIScrollView) { print("EmptyDataSet Will Disappear!") } func emptyDataSetDidDisappear(in scrollView: UIScrollView) { print("EmptyDataSet Did Disappear!") } } ================================================ FILE: Example/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 3.0.2 CFBundleSignature ???? CFBundleVersion 23 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015 Teambition Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # TBEmptyDataSet TBEmptyDataSet is an extension of UITableView/UICollectionView's super class, it will display a placeholder emptyDataSet when the data of tableView/collectionView is empty. TBEmptyDataSet can be composed of an image, a title and a description, or you can set it as a custom view. ![Example](Screenshots/Example.gif "Example") ## How To Get Started ### Carthage Specify "TBEmptyDataSet" in your ```Cartfile```: ```ogdl github "teambition/TBEmptyDataSet" ``` ### CocoaPods Specify "TBEmptyDataSet" in your ```Podfile```: ```ruby source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' use_frameworks! pod 'TBEmptyDataSet' ``` ### Usage ##### 1. Assign the data source and delegate ```swift tableView.emptyDataSetDataSource = self tableView.emptyDataSetDelegate = self ``` ##### 2. Implement the data source and delegate Data source functions: ```swift func imageForEmptyDataSet(in scrollView: UIScrollView) -> UIImage? { // return the image for EmptyDataSet } func titleForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { // return the title for EmptyDataSet } func descriptionForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { // return the description for EmptyDataSet } func imageTintColorForEmptyDataSet(in scrollView: UIScrollView) -> UIColor? { // return the image tint color for EmptyDataSet } func backgroundColorForEmptyDataSet(in scrollView: UIScrollView) -> UIColor? { // return the backgroundColor for EmptyDataSet } func verticalOffsetForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { // return the vertical offset for EmptyDataSet, default is 0 } func verticalSpacesForEmptyDataSet(in scrollView: UIScrollView) -> [CGFloat] { // return the vertical spaces from top to bottom for EmptyDataSet, default is [12, 12] } func titleMarginForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { // return the minimum horizontal margin space for title, default is 15 } func descriptionMarginForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { // return the minimum horizontal margin space for description, default is 15 } func customViewForEmptyDataSet(in scrollView: UIScrollView) -> UIView? { // return an UIView instance for EmptyDataSet } ``` Delegate functions: ```swift func emptyDataSetShouldDisplay(in scrollView: UIScrollView) -> Bool { // should display EmptyDataSet or not, default is true } func emptyDataSetTapEnabled(in scrollView: UIScrollView) -> Bool { // enable tap gesture or not, default is true } func emptyDataSetScrollEnabled(in scrollView: UIScrollView) -> Bool { // scrollView can scroll or not, default is false } func emptyDataSetDidTapEmptyView(in scrollView: UIScrollView) { // do something } func emptyDataSetWillAppear(in scrollView: UIScrollView) { // do something } func emptyDataSetDidAppear(in scrollView: UIScrollView) { // do something } func emptyDataSetWillDisappear(in scrollView: UIScrollView) { // do something } func emptyDataSetDidDisappear(in scrollView: UIScrollView) { // do something } ``` ##### 3. Data source events (inserting, deleting, reloading, etc.) TBEmptyDataSet will update automatically when the data source of table view or collection view changes. To be specific: * For UITableView, it updates when ```endUpdates()``` is called. * For both UITableView and UICollectionView, it updates when ```performBatchUpdates(_:completion:)``` is completed. * For both UITableView and UICollectionView, it updates when ```reloadData()``` is called. ## Minimum Requirement iOS 8.0 ## Release Notes * [Release Notes](https://github.com/teambition/TBEmptyDataSet/releases) ## License TBEmptyDataSet is released under the MIT license. See [LICENSE](https://github.com/teambition/TBEmptyDataSet/blob/master/LICENSE.md) for details. ## More Info Have a question? Please [open an issue](https://github.com/teambition/TBEmptyDataSet/issues/new)! ================================================ FILE: TBEmptyDataSet/Constants.swift ================================================ // // Constants.swift // TBEmptyDataSet // // Created by Xin Hong on 15/11/19. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit // swiftlint:disable variable_name internal struct AssociatedKeys { static var emptyDataSetDataSource = "emptyDataSetDataSource" static var emptyDataSetDelegate = "emptyDataSetDelegate" static var emptyDataView = "emptyDataView" } internal struct Selectors { static let tableViewSwizzledReloadData = #selector(UIScrollView.tb_tableViewSwizzledReloadData) static let tableViewSwizzledEndUpdates = #selector(UIScrollView.tb_tableViewSwizzledEndUpdates) @available(iOS 11.0, *) static let tableViewSwizzledPerformBatchUpdates = #selector(UIScrollView.tb_tableViewSwizzledPerformBatchUpdates(_:completion:)) static let collectionViewSwizzledReloadData = #selector(UIScrollView.tb_collectionViewSwizzledReloadData) static let collectionViewSwizzledPerformBatchUpdates = #selector(UIScrollView.tb_collectionViewSwizzledPerformBatchUpdates(_:completion:)) } internal struct TableViewSelectors { static let reloadData = #selector(UITableView.reloadData) static let endUpdates = #selector(UITableView.endUpdates) static let numberOfSections = #selector(UITableViewDataSource.numberOfSections(in:)) @available(iOS 11.0, *) static let performBatchUpdates = #selector(UITableView.performBatchUpdates(_:completion:)) } internal struct CollectionViewSelectors { static let reloadData = #selector(UICollectionView.reloadData) static let numberOfSections = #selector(UICollectionViewDataSource.numberOfSections(in:)) static let performBatchUpdates = #selector(UICollectionView.performBatchUpdates(_:completion:)) } internal struct DefaultValues { static let verticalOffset: CGFloat = 0 static let verticalSpace: CGFloat = 12 static let verticalSpaces = [verticalSpace, verticalSpace] static let titleMargin: CGFloat = 15 static let descriptionMargin: CGFloat = 15 } ================================================ FILE: TBEmptyDataSet/EmptyDataView.swift ================================================ // // EmptyDataView.swift // TBEmptyDataSet // // Created by Xin Hong on 15/11/19. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit internal class EmptyDataView: UIView { fileprivate struct ViewStrings { static let contentView = "contentView" static let imageView = "imageView" static let titleLabel = "titleLabel" static let descriptionLabel = "descriptionLabel" static let customView = "customView" } // MARK: - Properties internal lazy var contentView: UIView = { let contentView = UIView() contentView.translatesAutoresizingMaskIntoConstraints = false contentView.backgroundColor = .clear contentView.alpha = 0 return contentView }() internal lazy var imageView: UIImageView = { [unowned self] in let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.backgroundColor = .clear imageView.contentMode = .scaleAspectFill self.contentView.addSubview(imageView) return imageView }() internal lazy var titleLabel: UILabel = { [unowned self] in let titleLabel = UILabel() titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.backgroundColor = .clear titleLabel.font = .systemFont(ofSize: 27) titleLabel.textColor = UIColor(white: 0.6, alpha: 1) titleLabel.textAlignment = .center titleLabel.lineBreakMode = .byWordWrapping titleLabel.numberOfLines = 0 self.contentView.addSubview(titleLabel) return titleLabel }() internal lazy var descriptionLabel: UILabel = { [unowned self] in let descriptionLabel = UILabel() descriptionLabel.translatesAutoresizingMaskIntoConstraints = false descriptionLabel.backgroundColor = .clear descriptionLabel.font = .systemFont(ofSize: 17) descriptionLabel.textColor = UIColor(white: 0.6, alpha: 1) descriptionLabel.textAlignment = .center descriptionLabel.lineBreakMode = .byWordWrapping descriptionLabel.numberOfLines = 0 self.contentView.addSubview(descriptionLabel) return descriptionLabel }() internal var customView: UIView? { willSet { if let customView = customView { customView.removeFromSuperview() } } didSet { if let customView = customView { contentView.addSubview(customView) } } } internal var tapGesture: UITapGestureRecognizer? internal var verticalOffset = DefaultValues.verticalOffset internal var verticalSpaces = DefaultValues.verticalSpaces internal var titleMargin = DefaultValues.titleMargin internal var descriptionMargin = DefaultValues.descriptionMargin // MARK: - Helper fileprivate func shouldShowImageView() -> Bool { return imageView.image != nil } fileprivate func shouldShowTitleLabel() -> Bool { if let title = titleLabel.attributedText { return title.length > 0 } return false } fileprivate func shouldShowDescriptionLabel() -> Bool { if let description = descriptionLabel.attributedText { return description.length > 0 } return false } fileprivate func removeAllConstraints() { contentView.removeConstraints(contentView.constraints) removeConstraints(constraints) } // MARK: - View life cycle override init(frame: CGRect) { super.init(frame: frame) addSubview(self.contentView) } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override func didMoveToSuperview() { frame = super.bounds UIView.animate(withDuration: 0.25, animations: { () -> Void in self.contentView.alpha = 1 }) } // MARK: - Actions internal func setConstraints() { let centerX = NSLayoutConstraint(item: contentView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0) let centerY = NSLayoutConstraint(item: contentView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: verticalOffset) addConstraint(centerX) addConstraint(centerY) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[\(ViewStrings.contentView)]|", options: [], metrics: nil, views: [ViewStrings.contentView: contentView])) if let customView = customView { let translatesFrameIntoConstraints = customView.translatesAutoresizingMaskIntoConstraints customView.translatesAutoresizingMaskIntoConstraints = false if translatesFrameIntoConstraints { contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .width, relatedBy: .equal, toItem: .none, attribute: .notAnAttribute, multiplier: 1, constant: customView.frame.width)) contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .height, relatedBy: .equal, toItem: .none, attribute: .notAnAttribute, multiplier: 1, constant: customView.frame.height)) contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1, constant: 0)) contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[\(ViewStrings.customView)]|", options: [], metrics: nil, views: [ViewStrings.customView: customView])) } else { contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1, constant: 0)) contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .leading, relatedBy: .greaterThanOrEqual, toItem: contentView, attribute: .leading, multiplier: 1, constant: 0)) contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .trailing, relatedBy: .lessThanOrEqual, toItem: contentView, attribute: .trailing, multiplier: 1, constant: 0)) contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[\(ViewStrings.customView)]|", options: [], metrics: nil, views: [ViewStrings.customView: customView])) } } else { var viewStrings = [String]() var views = [String: UIView]() if shouldShowImageView() { if imageView.superview == nil { contentView.addSubview(imageView) } let viewString = ViewStrings.imageView viewStrings.append(viewString) views[viewString] = imageView contentView.addConstraint(NSLayoutConstraint(item: imageView, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1, constant: 0)) } else { imageView.removeFromSuperview() } if shouldShowTitleLabel() { if titleLabel.superview == nil { contentView.addSubview(titleLabel) } let viewString = ViewStrings.titleLabel viewStrings.append(viewString) views[viewString] = titleLabel contentView.addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1, constant: 0)) contentView.addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .leading, relatedBy: .greaterThanOrEqual, toItem: contentView, attribute: .leading, multiplier: 1, constant: titleMargin)) contentView.addConstraint(NSLayoutConstraint(item: contentView, attribute: .trailing, relatedBy: .greaterThanOrEqual, toItem: titleLabel, attribute: .trailing, multiplier: 1, constant: titleMargin)) } else { titleLabel.removeFromSuperview() } if shouldShowDescriptionLabel() { if descriptionLabel.superview == nil { contentView.addSubview(descriptionLabel) } let viewString = ViewStrings.descriptionLabel viewStrings.append(viewString) views[viewString] = descriptionLabel contentView.addConstraint(NSLayoutConstraint(item: descriptionLabel, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1, constant: 0)) contentView.addConstraint(NSLayoutConstraint(item: descriptionLabel, attribute: .leading, relatedBy: .greaterThanOrEqual, toItem: contentView, attribute: .leading, multiplier: 1, constant: descriptionMargin)) contentView.addConstraint(NSLayoutConstraint(item: contentView, attribute: .trailing, relatedBy: .greaterThanOrEqual, toItem: descriptionLabel, attribute: .trailing, multiplier: 1, constant: descriptionMargin)) } else { descriptionLabel.removeFromSuperview() } var verticalFormat = String() for (index, viewString) in viewStrings.enumerated() { verticalFormat += "[\(viewString)]" if index != viewStrings.count - 1 { let verticalSpace = index < verticalSpaces.count ? verticalSpaces[index] : DefaultValues.verticalSpace verticalFormat += "-(\(verticalSpace))-" } } if !verticalFormat.isEmpty { contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|\(verticalFormat)|", options: [], metrics: nil, views: views)) } } } internal func prepareForDisplay() { imageView.image = nil titleLabel.attributedText = nil descriptionLabel.attributedText = nil customView = nil removeAllConstraints() } internal func reset() { imageView.image = nil titleLabel.attributedText = nil descriptionLabel.attributedText = nil customView = nil tapGesture = nil removeAllConstraints() } } ================================================ FILE: TBEmptyDataSet/Protocols.swift ================================================ // // Protocols.swift // TBEmptyDataSet // // Created by Xin Hong on 15/11/19. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit public protocol TBEmptyDataSetDataSource { func imageForEmptyDataSet(in scrollView: UIScrollView) -> UIImage? func titleForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? func descriptionForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? func imageTintColorForEmptyDataSet(in scrollView: UIScrollView) -> UIColor? func backgroundColorForEmptyDataSet(in scrollView: UIScrollView) -> UIColor? func verticalOffsetForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat func verticalSpacesForEmptyDataSet(in scrollView: UIScrollView) -> [CGFloat] func titleMarginForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat func descriptionMarginForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat func customViewForEmptyDataSet(in scrollView: UIScrollView) -> UIView? } public protocol TBEmptyDataSetDelegate { func emptyDataSetShouldDisplay(in scrollView: UIScrollView) -> Bool func emptyDataSetTapEnabled(in scrollView: UIScrollView) -> Bool func emptyDataSetScrollEnabled(in scrollView: UIScrollView) -> Bool func emptyDataSetDidTapEmptyView(in scrollView: UIScrollView) func emptyDataSetWillAppear(in scrollView: UIScrollView) func emptyDataSetDidAppear(in scrollView: UIScrollView) func emptyDataSetWillDisappear(in scrollView: UIScrollView) func emptyDataSetDidDisappear(in scrollView: UIScrollView) } public extension TBEmptyDataSetDataSource { func imageForEmptyDataSet(in scrollView: UIScrollView) -> UIImage? { return nil } func titleForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { return nil } func descriptionForEmptyDataSet(in scrollView: UIScrollView) -> NSAttributedString? { return nil } func imageTintColorForEmptyDataSet(in scrollView: UIScrollView) -> UIColor? { return nil } func backgroundColorForEmptyDataSet(in scrollView: UIScrollView) -> UIColor? { return nil } func verticalOffsetForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { return DefaultValues.verticalOffset } func verticalSpacesForEmptyDataSet(in scrollView: UIScrollView) -> [CGFloat] { return DefaultValues.verticalSpaces } func titleMarginForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { return DefaultValues.titleMargin } func descriptionMarginForEmptyDataSet(in scrollView: UIScrollView) -> CGFloat { return DefaultValues.descriptionMargin } func customViewForEmptyDataSet(in scrollView: UIScrollView) -> UIView? { return nil } } public extension TBEmptyDataSetDelegate { func emptyDataSetShouldDisplay(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetTapEnabled(in scrollView: UIScrollView) -> Bool { return true } func emptyDataSetScrollEnabled(in scrollView: UIScrollView) -> Bool { return false } func emptyDataSetDidTapEmptyView(in scrollView: UIScrollView) { } func emptyDataSetWillAppear(in scrollView: UIScrollView) { } func emptyDataSetDidAppear(in scrollView: UIScrollView) { } func emptyDataSetWillDisappear(in scrollView: UIScrollView) { } func emptyDataSetDidDisappear(in scrollView: UIScrollView) { } } ================================================ FILE: TBEmptyDataSet/Supporting Files/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 3.0.2 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: TBEmptyDataSet/TBEmptyDataSet.swift ================================================ // // TBEmptyDataSet.swift // TBEmptyDataSet // // Created by Xin Hong on 15/11/19. // Copyright © 2015年 Teambition. All rights reserved. // import UIKit extension UIScrollView { // MARK: - Properties public var emptyDataSetDataSource: TBEmptyDataSetDataSource? { get { let container = objc_getAssociatedObject(self, &AssociatedKeys.emptyDataSetDataSource) as? WeakObjectContainer return container?.object as? TBEmptyDataSetDataSource } set { if let newValue = newValue { objc_setAssociatedObject(self, &AssociatedKeys.emptyDataSetDataSource, WeakObjectContainer(object: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) switch self { case is UITableView: UITableView.tb_swizzleTableViewReloadData UITableView.tb_swizzleTableViewEndUpdates if #available(iOS 11.0, *) { UITableView.tb_swizzleTableViewPerformBatchUpdates } case is UICollectionView: UICollectionView.tb_swizzleCollectionViewReloadData UICollectionView.tb_swizzleCollectionViewPerformBatchUpdates default: break } } else { handlingInvalidEmptyDataSet() } } } public var emptyDataSetDelegate: TBEmptyDataSetDelegate? { get { let container = objc_getAssociatedObject(self, &AssociatedKeys.emptyDataSetDelegate) as? WeakObjectContainer return container?.object as? TBEmptyDataSetDelegate } set { if let newValue = newValue { objc_setAssociatedObject(self, &AssociatedKeys.emptyDataSetDelegate, WeakObjectContainer(object: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { handlingInvalidEmptyDataSet() } } } internal var emptyDataView: EmptyDataView? { get { return objc_getAssociatedObject(self, &AssociatedKeys.emptyDataView) as? EmptyDataView } set { objc_setAssociatedObject(self, &AssociatedKeys.emptyDataView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } // MARK: - Public public var emptyDataViewVisible: Bool { if let emptyDataView = emptyDataView { return !emptyDataView.isHidden } return false } public func updateEmptyDataSetIfNeeded() { reloadEmptyDataSet() } // MARK: - Data source and delegate getters fileprivate func emptyDataSetImage() -> UIImage? { return emptyDataSetDataSource?.imageForEmptyDataSet(in: self) } fileprivate func emptyDataSetTitle() -> NSAttributedString? { return emptyDataSetDataSource?.titleForEmptyDataSet(in: self) } fileprivate func emptyDataSetDescription() -> NSAttributedString? { return emptyDataSetDataSource?.descriptionForEmptyDataSet(in: self) } fileprivate func emptyDataSetImageTintColor() -> UIColor? { return emptyDataSetDataSource?.imageTintColorForEmptyDataSet(in: self) } fileprivate func emptyDataSetBackgroundColor() -> UIColor? { return emptyDataSetDataSource?.backgroundColorForEmptyDataSet(in: self) } fileprivate func emptyDataSetVerticalOffset() -> CGFloat { return emptyDataSetDataSource?.verticalOffsetForEmptyDataSet(in: self) ?? DefaultValues.verticalOffset } fileprivate func emptyDataSetVerticalSpaces() -> [CGFloat] { return emptyDataSetDataSource?.verticalSpacesForEmptyDataSet(in: self) ?? DefaultValues.verticalSpaces } fileprivate func emptyDataSetTitleMargin() -> CGFloat { return emptyDataSetDataSource?.titleMarginForEmptyDataSet(in: self) ?? DefaultValues.titleMargin } fileprivate func emptyDataSetDescriptionMargin() -> CGFloat { return emptyDataSetDataSource?.descriptionMarginForEmptyDataSet(in: self) ?? DefaultValues.descriptionMargin } fileprivate func emptyDataSetCustomView() -> UIView? { return emptyDataSetDataSource?.customViewForEmptyDataSet(in: self) } fileprivate func emptyDataSetShouldDisplay() -> Bool { return emptyDataSetDelegate?.emptyDataSetShouldDisplay(in: self) ?? true } fileprivate func emptyDataSetTapEnabled() -> Bool { return emptyDataSetDelegate?.emptyDataSetTapEnabled(in: self) ?? true } fileprivate func emptyDataSetScrollEnabled() -> Bool { return emptyDataSetDelegate?.emptyDataSetScrollEnabled(in: self) ?? false } // MARK: - Helper @objc func didTapEmptyDataView(_ sender: Any) { emptyDataSetDelegate?.emptyDataSetDidTapEmptyView(in: self) } fileprivate func makeEmptyDataView() -> EmptyDataView { let emptyDataView = EmptyDataView(frame: frame) emptyDataView.autoresizingMask = [.flexibleHeight, .flexibleWidth] emptyDataView.isHidden = true let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapEmptyDataView(_:))) emptyDataView.addGestureRecognizer(tapGesture) emptyDataView.tapGesture = tapGesture return emptyDataView } fileprivate func emptyDataSetAvailable() -> Bool { if emptyDataSetDataSource != nil { return (self is UITableView) || (self is UICollectionView) } return false } fileprivate func cellsCount() -> Int { var count = 0 if let tableView = self as? UITableView { if let dataSource = tableView.dataSource { if dataSource.responds(to: TableViewSelectors.numberOfSections) { let sections = dataSource.numberOfSections?(in: tableView) ?? 0 for section in 0.. 1 { insertSubview(emptyDataView, at: 0) } else { addSubview(emptyDataView) } } emptyDataView.prepareForDisplay() emptyDataView.verticalOffset = emptyDataSetVerticalOffset() emptyDataView.verticalSpaces = emptyDataSetVerticalSpaces() emptyDataView.titleMargin = emptyDataSetTitleMargin() emptyDataView.descriptionMargin = emptyDataSetDescriptionMargin() if let customView = emptyDataSetCustomView() { emptyDataView.customView = customView } else { if let imageTintColor = emptyDataSetImageTintColor() { emptyDataView.imageView.image = emptyDataSetImage()?.withRenderingMode(.alwaysTemplate) emptyDataView.imageView.tintColor = imageTintColor } else { emptyDataView.imageView.image = emptyDataSetImage()?.withRenderingMode(.alwaysOriginal) } emptyDataView.titleLabel.attributedText = emptyDataSetTitle() emptyDataView.descriptionLabel.attributedText = emptyDataSetDescription() } emptyDataView.backgroundColor = emptyDataSetBackgroundColor() emptyDataView.isHidden = false emptyDataView.clipsToBounds = true emptyDataView.tapGesture?.isEnabled = emptyDataSetTapEnabled() isScrollEnabled = emptyDataSetScrollEnabled() emptyDataView.setConstraints() emptyDataView.layoutIfNeeded() emptyDataSetDelegate?.emptyDataSetDidAppear(in: self) } // MARK: - Method swizzling fileprivate class func tb_swizzleMethod(for aClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) { guard let originalMethod = class_getInstanceMethod(aClass, originalSelector), let swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector) else { return } 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) } } // swiftlint:disable variable_name fileprivate static let tb_swizzleTableViewReloadData: () = { let originalSelector = TableViewSelectors.reloadData let swizzledSelector = Selectors.tableViewSwizzledReloadData tb_swizzleMethod(for: UITableView.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector) }() // swiftlint:disable variable_name fileprivate static let tb_swizzleTableViewEndUpdates: () = { let originalSelector = TableViewSelectors.endUpdates let swizzledSelector = Selectors.tableViewSwizzledEndUpdates tb_swizzleMethod(for: UITableView.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector) }() // swiftlint:disable variable_name @available(iOS 11.0, *) fileprivate static let tb_swizzleTableViewPerformBatchUpdates: () = { let originalSelector = TableViewSelectors.performBatchUpdates let swizzledSelector = Selectors.tableViewSwizzledPerformBatchUpdates tb_swizzleMethod(for: UITableView.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector) }() // swiftlint:disable variable_name fileprivate static let tb_swizzleCollectionViewReloadData: () = { let originalSelector = CollectionViewSelectors.reloadData let swizzledSelector = Selectors.collectionViewSwizzledReloadData tb_swizzleMethod(for: UICollectionView.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector) }() // swiftlint:disable variable_name fileprivate static let tb_swizzleCollectionViewPerformBatchUpdates: () = { let originalSelector = CollectionViewSelectors.performBatchUpdates let swizzledSelector = Selectors.collectionViewSwizzledPerformBatchUpdates tb_swizzleMethod(for: UICollectionView.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector) }() @objc func tb_tableViewSwizzledReloadData() { tb_tableViewSwizzledReloadData() reloadEmptyDataSet() } @objc func tb_tableViewSwizzledEndUpdates() { tb_tableViewSwizzledEndUpdates() reloadEmptyDataSet() } @available(iOS 11.0, *) @objc func tb_tableViewSwizzledPerformBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)?) { tb_tableViewSwizzledPerformBatchUpdates(updates) { [weak self](completed) in completion?(completed) self?.reloadEmptyDataSet() } } @objc func tb_collectionViewSwizzledReloadData() { tb_collectionViewSwizzledReloadData() reloadEmptyDataSet() } @objc func tb_collectionViewSwizzledPerformBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)?) { tb_collectionViewSwizzledPerformBatchUpdates(updates) { [weak self](completed) in completion?(completed) self?.reloadEmptyDataSet() } } } ================================================ FILE: TBEmptyDataSet/WeakObjectContainer.swift ================================================ // // WeakObjectContainer.swift // TBEmptyDataSet // // Created by Xin Hong on 2016/11/4. // Copyright © 2016年 Teambition. All rights reserved. // import UIKit class WeakObjectContainer: NSObject { weak var object: AnyObject? init(object: Any) { super.init() self.object = object as AnyObject } } ================================================ FILE: TBEmptyDataSet.podspec ================================================ Pod::Spec.new do |s| s.name = "TBEmptyDataSet" s.version = "3.0.2" s.summary = "An extension of UITableView/UICollectionView's super class, it will display a placeholder when the data is empty." s.homepage = "https://github.com/teambition/TBEmptyDataSet" s.license = { :type => "MIT", :file => "LICENSE.md" } s.author = "Xin Hong" s.source = { :git => "https://github.com/teambition/TBEmptyDataSet.git", :tag => s.version.to_s } s.source_files = "TBEmptyDataSet/*.swift" s.platform = :ios, "8.0" s.requires_arc = true s.frameworks = "Foundation", "UIKit" end ================================================ FILE: TBEmptyDataSet.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ D331287B1DCC259C00773E72 /* WeakObjectContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D331287A1DCC259C00773E72 /* WeakObjectContainer.swift */; }; D33C7C741C44DCA000E1687A /* TBEmptyDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33C7C731C44DCA000E1687A /* TBEmptyDataSet.swift */; }; D33C7C781C44DF0A00E1687A /* EmptyDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33C7C771C44DF0A00E1687A /* EmptyDataView.swift */; }; D33C7C7A1C44E17000E1687A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33C7C791C44E17000E1687A /* Constants.swift */; }; D33C7C7C1C44E21700E1687A /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33C7C7B1C44E21700E1687A /* Protocols.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ D331287A1DCC259C00773E72 /* WeakObjectContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakObjectContainer.swift; sourceTree = ""; }; D33C7C671C44DBED00E1687A /* TBEmptyDataSet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TBEmptyDataSet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D33C7C731C44DCA000E1687A /* TBEmptyDataSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TBEmptyDataSet.swift; sourceTree = ""; }; D33C7C751C44DCC200E1687A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D33C7C771C44DF0A00E1687A /* EmptyDataView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyDataView.swift; sourceTree = ""; }; D33C7C791C44E17000E1687A /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; D33C7C7B1C44E21700E1687A /* Protocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Protocols.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ D33C7C631C44DBED00E1687A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ D33C7C5D1C44DBED00E1687A = { isa = PBXGroup; children = ( D33C7C691C44DBED00E1687A /* TBEmptyDataSet */, D33C7C721C44DC8B00E1687A /* Supporting Files */, D33C7C681C44DBED00E1687A /* Products */, ); sourceTree = ""; }; D33C7C681C44DBED00E1687A /* Products */ = { isa = PBXGroup; children = ( D33C7C671C44DBED00E1687A /* TBEmptyDataSet.framework */, ); name = Products; sourceTree = ""; }; D33C7C691C44DBED00E1687A /* TBEmptyDataSet */ = { isa = PBXGroup; children = ( D33C7C791C44E17000E1687A /* Constants.swift */, D33C7C7B1C44E21700E1687A /* Protocols.swift */, D33C7C771C44DF0A00E1687A /* EmptyDataView.swift */, D33C7C731C44DCA000E1687A /* TBEmptyDataSet.swift */, D331287A1DCC259C00773E72 /* WeakObjectContainer.swift */, ); path = TBEmptyDataSet; sourceTree = ""; }; D33C7C721C44DC8B00E1687A /* Supporting Files */ = { isa = PBXGroup; children = ( D33C7C751C44DCC200E1687A /* Info.plist */, ); name = "Supporting Files"; path = "TBEmptyDataSet/Supporting Files"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ D33C7C641C44DBED00E1687A /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ D33C7C661C44DBED00E1687A /* TBEmptyDataSet */ = { isa = PBXNativeTarget; buildConfigurationList = D33C7C6F1C44DBED00E1687A /* Build configuration list for PBXNativeTarget "TBEmptyDataSet" */; buildPhases = ( D33C7C621C44DBED00E1687A /* Sources */, D33C7C631C44DBED00E1687A /* Frameworks */, D33C7C641C44DBED00E1687A /* Headers */, D33C7C651C44DBED00E1687A /* Resources */, ); buildRules = ( ); dependencies = ( ); name = TBEmptyDataSet; productName = TBEmptyDataSet; productReference = D33C7C671C44DBED00E1687A /* TBEmptyDataSet.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ D33C7C5E1C44DBED00E1687A /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1020; ORGANIZATIONNAME = Teambition; TargetAttributes = { D33C7C661C44DBED00E1687A = { CreatedOnToolsVersion = 7.2; LastSwiftMigration = 1020; }; }; }; buildConfigurationList = D33C7C611C44DBED00E1687A /* Build configuration list for PBXProject "TBEmptyDataSet" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = D33C7C5D1C44DBED00E1687A; productRefGroup = D33C7C681C44DBED00E1687A /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D33C7C661C44DBED00E1687A /* TBEmptyDataSet */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ D33C7C651C44DBED00E1687A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ D33C7C621C44DBED00E1687A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D33C7C781C44DF0A00E1687A /* EmptyDataView.swift in Sources */, D331287B1DCC259C00773E72 /* WeakObjectContainer.swift in Sources */, D33C7C7C1C44E21700E1687A /* Protocols.swift in Sources */, D33C7C741C44DCA000E1687A /* TBEmptyDataSet.swift in Sources */, D33C7C7A1C44E17000E1687A /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ D33C7C6D1C44DBED00E1687A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; D33C7C6E1C44DBED00E1687A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; D33C7C701C44DBED00E1687A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/TBEmptyDataSet/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = Teambition.TBEmptyDataSet; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; D33C7C711C44DBED00E1687A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/TBEmptyDataSet/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = Teambition.TBEmptyDataSet; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ D33C7C611C44DBED00E1687A /* Build configuration list for PBXProject "TBEmptyDataSet" */ = { isa = XCConfigurationList; buildConfigurations = ( D33C7C6D1C44DBED00E1687A /* Debug */, D33C7C6E1C44DBED00E1687A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D33C7C6F1C44DBED00E1687A /* Build configuration list for PBXNativeTarget "TBEmptyDataSet" */ = { isa = XCConfigurationList; buildConfigurations = ( D33C7C701C44DBED00E1687A /* Debug */, D33C7C711C44DBED00E1687A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = D33C7C5E1C44DBED00E1687A /* Project object */; } ================================================ FILE: TBEmptyDataSet.xcodeproj/xcshareddata/xcschemes/TBEmptyDataSet.xcscheme ================================================ ================================================ FILE: TBEmptyDataSet.xcodeproj/xcuserdata/hongxin.xcuserdatad/xcschemes/xcschememanagement.plist ================================================ SchemeUserState TBEmptyDataSet.xcscheme_^#shared#^_ orderHint 0 SuppressBuildableAutocreation D33C7C661C44DBED00E1687A primary ================================================ FILE: TBEmptyDataSet.xcodeproj/xcuserdata/zetasq.xcuserdatad/xcschemes/xcschememanagement.plist ================================================ SchemeUserState TBEmptyDataSet.xcscheme_^#shared#^_ orderHint 0 SuppressBuildableAutocreation D33C7C661C44DBED00E1687A primary ================================================ FILE: TBEmptyDataSet.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: TBEmptyDataSet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: TBEmptyDataSetExample.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ D3013DFB1C06B7FA00266EE2 /* DemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3013DFA1C06B7FA00266EE2 /* DemoViewController.swift */; }; D33C7C801C44E9A800E1687A /* TBEmptyDataSet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D33C7C7F1C44E9A800E1687A /* TBEmptyDataSet.framework */; }; D33C7C811C44E9A800E1687A /* TBEmptyDataSet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D33C7C7F1C44E9A800E1687A /* TBEmptyDataSet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D3565AA31BFD9449002CFA16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3565AA21BFD9449002CFA16 /* AppDelegate.swift */; }; D3565AA81BFD9449002CFA16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D3565AA61BFD9449002CFA16 /* Main.storyboard */; }; D3565AAA1BFD9449002CFA16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D3565AA91BFD9449002CFA16 /* Assets.xcassets */; }; D39EBFCA1C040C040089B394 /* EmptyDataDemoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39EBFC91C040C040089B394 /* EmptyDataDemoTableViewController.swift */; }; D39EBFCC1C040F220089B394 /* EmptyDataDemoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39EBFCB1C040F220089B394 /* EmptyDataDemoCollectionViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ D33C7C821C44E9A800E1687A /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( D33C7C811C44E9A800E1687A /* TBEmptyDataSet.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ D3013DFA1C06B7FA00266EE2 /* DemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoViewController.swift; sourceTree = ""; }; D33C7C7F1C44E9A800E1687A /* TBEmptyDataSet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = TBEmptyDataSet.framework; path = "/Users/hongxin/Library/Developer/Xcode/DerivedData/TBEmptyDataSet-cgdivxseqtlgnzctplvlotcjtyby/Build/Products/Debug-iphoneos/TBEmptyDataSet.framework"; sourceTree = ""; }; D3565A9F1BFD9449002CFA16 /* TBEmptyDataSetExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TBEmptyDataSetExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; D3565AA21BFD9449002CFA16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D3565AA71BFD9449002CFA16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D3565AA91BFD9449002CFA16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D3565AAE1BFD9449002CFA16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D39EBFC91C040C040089B394 /* EmptyDataDemoTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyDataDemoTableViewController.swift; sourceTree = ""; }; D39EBFCB1C040F220089B394 /* EmptyDataDemoCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyDataDemoCollectionViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ D3565A9C1BFD9449002CFA16 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D33C7C801C44E9A800E1687A /* TBEmptyDataSet.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ D3565A961BFD9449002CFA16 = { isa = PBXGroup; children = ( D33C7C7F1C44E9A800E1687A /* TBEmptyDataSet.framework */, D3565AA11BFD9449002CFA16 /* Example */, D3565AA01BFD9449002CFA16 /* Products */, ); sourceTree = ""; }; D3565AA01BFD9449002CFA16 /* Products */ = { isa = PBXGroup; children = ( D3565A9F1BFD9449002CFA16 /* TBEmptyDataSetExample.app */, ); name = Products; sourceTree = ""; }; D3565AA11BFD9449002CFA16 /* Example */ = { isa = PBXGroup; children = ( D3565AA21BFD9449002CFA16 /* AppDelegate.swift */, D3013DFA1C06B7FA00266EE2 /* DemoViewController.swift */, D39EBFC91C040C040089B394 /* EmptyDataDemoTableViewController.swift */, D39EBFCB1C040F220089B394 /* EmptyDataDemoCollectionViewController.swift */, D3565AA61BFD9449002CFA16 /* Main.storyboard */, D3565AA91BFD9449002CFA16 /* Assets.xcassets */, D3565AAE1BFD9449002CFA16 /* Info.plist */, ); path = Example; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ D3565A9E1BFD9449002CFA16 /* TBEmptyDataSetExample */ = { isa = PBXNativeTarget; buildConfigurationList = D3565AB11BFD9449002CFA16 /* Build configuration list for PBXNativeTarget "TBEmptyDataSetExample" */; buildPhases = ( D3565A9B1BFD9449002CFA16 /* Sources */, D3565A9C1BFD9449002CFA16 /* Frameworks */, D3565A9D1BFD9449002CFA16 /* Resources */, D3013DFC1C06E0D200266EE2 /* ShellScript */, D33C7C821C44E9A800E1687A /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = TBEmptyDataSetExample; productName = TBEmptyDataSet; productReference = D3565A9F1BFD9449002CFA16 /* TBEmptyDataSetExample.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ D3565A971BFD9449002CFA16 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0710; LastUpgradeCheck = 1020; ORGANIZATIONNAME = Teambition; TargetAttributes = { D3565A9E1BFD9449002CFA16 = { CreatedOnToolsVersion = 7.1.1; LastSwiftMigration = 1020; }; }; }; buildConfigurationList = D3565A9A1BFD9449002CFA16 /* Build configuration list for PBXProject "TBEmptyDataSetExample" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = D3565A961BFD9449002CFA16; productRefGroup = D3565AA01BFD9449002CFA16 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D3565A9E1BFD9449002CFA16 /* TBEmptyDataSetExample */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ D3565A9D1BFD9449002CFA16 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( D3565AAA1BFD9449002CFA16 /* Assets.xcassets in Resources */, D3565AA81BFD9449002CFA16 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ D3013DFC1C06E0D200266EE2 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ D3565A9B1BFD9449002CFA16 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D3565AA31BFD9449002CFA16 /* AppDelegate.swift in Sources */, D39EBFCA1C040C040089B394 /* EmptyDataDemoTableViewController.swift in Sources */, D39EBFCC1C040F220089B394 /* EmptyDataDemoCollectionViewController.swift in Sources */, D3013DFB1C06B7FA00266EE2 /* DemoViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ D3565AA61BFD9449002CFA16 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( D3565AA71BFD9449002CFA16 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ D3565AAF1BFD9449002CFA16 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; D3565AB01BFD9449002CFA16 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; D3565AB21BFD9449002CFA16 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = Teambition.TBEmptyDataSetExample; PRODUCT_NAME = TBEmptyDataSetExample; SWIFT_VERSION = 5.0; }; name = Debug; }; D3565AB31BFD9449002CFA16 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = Teambition.TBEmptyDataSetExample; PRODUCT_NAME = TBEmptyDataSetExample; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ D3565A9A1BFD9449002CFA16 /* Build configuration list for PBXProject "TBEmptyDataSetExample" */ = { isa = XCConfigurationList; buildConfigurations = ( D3565AAF1BFD9449002CFA16 /* Debug */, D3565AB01BFD9449002CFA16 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D3565AB11BFD9449002CFA16 /* Build configuration list for PBXNativeTarget "TBEmptyDataSetExample" */ = { isa = XCConfigurationList; buildConfigurations = ( D3565AB21BFD9449002CFA16 /* Debug */, D3565AB31BFD9449002CFA16 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = D3565A971BFD9449002CFA16 /* Project object */; } ================================================ FILE: TBEmptyDataSetExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: TBEmptyDataSetExample.xcodeproj/xcuserdata/hongxin.xcuserdatad/xcschemes/TBEmptyDataSetExample.xcscheme ================================================ ================================================ FILE: TBEmptyDataSetExample.xcodeproj/xcuserdata/hongxin.xcuserdatad/xcschemes/xcschememanagement.plist ================================================ SchemeUserState TBEmptyDataSetExample.xcscheme orderHint 0 SuppressBuildableAutocreation D3565A9E1BFD9449002CFA16 primary ================================================ FILE: TBEmptyDataSetExample.xcodeproj/xcuserdata/zetasq.xcuserdatad/xcschemes/TBEmptyDataSetExample.xcscheme ================================================ ================================================ FILE: TBEmptyDataSetExample.xcodeproj/xcuserdata/zetasq.xcuserdatad/xcschemes/xcschememanagement.plist ================================================ SchemeUserState TBEmptyDataSetExample.xcscheme orderHint 1 SuppressBuildableAutocreation D3565A9E1BFD9449002CFA16 primary