Handling an empty UITableView. Print a friendly message

前端 未结 22 1055
青春惊慌失措
青春惊慌失措 2020-12-04 05:22

I have a UITableView that in some cases it is legal to be empty. So instead of showing the background image of the app, I would prefer to print a friendly message in the scr

相关标签:
22条回答
  • 2020-12-04 06:00

    Same as Jhonston's answer, but I preferred it as an extension:

    import UIKit
    
    extension UITableView {
    
        func setEmptyMessage(_ message: String) {
            let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
            messageLabel.text = message
            messageLabel.textColor = .black
            messageLabel.numberOfLines = 0
            messageLabel.textAlignment = .center
            messageLabel.font = UIFont(name: "TrebuchetMS", size: 15)
            messageLabel.sizeToFit()
    
            self.backgroundView = messageLabel
            self.separatorStyle = .none
        }
    
        func restore() {
            self.backgroundView = nil
            self.separatorStyle = .singleLine
        }
    }
    

    Usage:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if things.count == 0 {
            self.tableView.setEmptyMessage("My Message")
        } else {
            self.tableView.restore()
        }
    
        return things.count
    }
    
    0 讨论(0)
  • 2020-12-04 06:01

    One way of doing it would be modifying your data source to return 1 when the number of rows is zero, and to produce a special-purpose cell (perhaps with a different cell identifier) in the tableView:cellForRowAtIndexPath: method.

    -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
        NSInteger actualNumberOfRows = <calculate the actual number of rows>;
        return (actualNumberOfRows  == 0) ? 1 : actualNumberOfRows;
    }
    
    -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        NSInteger actualNumberOfRows = <calculate the actual number of rows>;
        if (actualNumberOfRows == 0) {
            // Produce a special cell with the "list is now empty" message
        }
        // Produce the correct cell the usual way
        ...
    }
    

    This may get somewhat complicated if you have multiple table view controllers that you need to maintain, because someone will eventually forget to insert a zero check. A better approach is to create a separate implementation of a UITableViewDataSource implementation that always returns a single row with a configurable message (let's call it EmptyTableViewDataSource). When the data that is managed by your table view controller changes, the code that manages the change would check if the data is empty. If it is not empty, set your table view controller with its regular data source; otherwise, set it with an instance of the EmptyTableViewDataSource that has been configured with the appropriate message.

    0 讨论(0)
  • 2020-12-04 06:03

    There is a specific use case for multiple data sets and sections, where you need an empty state for each section.

    You can use suggestions mentioned in multiple answers to this question - provide custom empty state cell.

    I'll try to walk you through all the steps programmatically in more detail and hopefully, this will be helpful. Here's the result we can expect:

    For simplicity's sake, we will work with 2 data sets (2 sections), those will be static.

    I will also assume that you have the rest of your tableView logic working properly with datasets, tabvleView cells, and sections.

    Swift 5, let's do it:

    1. Create a custom empty state UITableViewCell class:

    class EmptyTableViewCell: UITableViewCell {
    
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            setupView()
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        let label: UILabel = {
            let label = UILabel()
            label.translatesAutoresizingMaskIntoConstraints = false
            label.text = "Empty State Message"
            label.font = .systemFont(ofSize: 16)
            label.textColor = .gray
            label.textAlignment = .left
            label.numberOfLines = 1
            return label
        }()
    
        private func setupView(){
            contentView.addSubviews(label)
            let layoutGuide = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                label.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
                label.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
                label.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor),
                label.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
                label.heightAnchor.constraint(equalToConstant: 50)
            ])
        }
    }
    

    2. Add the following to your UITableViewController class to register your empty cell:

    class TableViewController: UITableViewController {
        ...
        let emptyCellReuseIdentifier = "emptyCellReuseIdentifier"
        ...
        override func viewDidLoad(){
            ...
            tableView.register(EmptyTableViewCell.self, forCellReuseIdentifier: emptyCellReuseIdentifier)
            ...
        }
    }
    

    3. Now let's highlight some assumptions mentioned above:

    class TableViewController: UITableViewController {
        // 2 Data Sets
        var firstDataSet: [String] = []
        var secondDataSet: [String] = []
    
        // Sections array
        let sections: [SectionHeader] = [
            .init(id: 0, title: "First Section"),
            .init(id: 1, title: "Second Section")
        ]
        ...
        // MARK: - Table view data source
        override func numberOfSections(in tableView: UITableView) -> Int {
            sections.count
        }
        override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
            return sections[section].title
        }
            ...
        }
    
    struct SectionHeader {
        let id: Int
        let title: String
    }
    

    4. Now let's add some custom logic to our Data Source to handle Empty Rows in our sections. Here we are returning 1 row if a data set is empty:

    class TableViewController: UITableViewController {
        ...
        // MARK: - Table view data source
        ...
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            switch section{
            case 0:
                let numberOfRows = firstDataSet.isEmpty ? 1 : firstDataSet.count
                return numberOfRows          
            case 1:
                let numberOfRows = secondDataSet.isEmpty ? 1 : secondDataSet.count
                return numberOfRows   
            default:
                return 0
            }
        }
        ...
    }
    

    5. Lastly, the most important "cellForRowAt indexPath":

    class TableViewController: UITableViewController {
        ...
        // MARK: - Table view data source
        ...
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            
            // Handle Empty Rows State
            switch indexPath.section {
            case 0:
                if firstDataSet.isEmpty {
                    if let cell = tableView.dequeueReusableCell(withIdentifier: emptyCellReuseIdentifier) as? EmptyTableViewCell {
                        cell.label.text = "First Data Set Is Empty"
                        return cell
                    }
                }
            case 1:
                if secondDataSet.isEmpty {
                    if let cell = tableView.dequeueReusableCell(withIdentifier: emptyCellReuseIdentifier) as? EmptyTableViewCell {
                        cell.label.text = "Second Data Set Is Empty"
                        return cell
                    }
                }
            default:
                break
            }
            
            // Handle Existing Data Sets
            if let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as? TableViewCell {
                switch indexPath.section {
                case 0:
                    ...
                case 1:
                    ...
                default:
                    break
                }
                return cell
            }
    
            return UITableViewCell()
        }
        ...
    }
    
    0 讨论(0)
  • 2020-12-04 06:05

    Select your tableviewController Scene in storyboard

    Drag and drop UIView Add label with your message (eg: No Data)

    create outlet of UIView (say for eg yournoDataView) on your TableViewController.

    and in viewDidLoad

    self.tableView.backgroundView = yourNoDataView

    0 讨论(0)
  • 2020-12-04 06:07

    Swift version but better and simpler form . **3.0

    I hope it server your purpose......

    In your UITableViewController .

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if searchController.isActive && searchController.searchBar.text != "" {
            if filteredContacts.count > 0 {
                self.tableView.backgroundView = .none;
                return filteredContacts.count
            } else {
                Helper.EmptyMessage(message: ConstantMap.NO_CONTACT_FOUND, viewController: self)
                return 0
            }
        } else {
            if contacts.count > 0 {
                self.tableView.backgroundView = .none;
                return contacts.count
            } else {
                Helper.EmptyMessage(message: ConstantMap.NO_CONTACT_FOUND, viewController: self)
                return 0
            }
        }
    }
    

    Helper Class with function :

     /* Description: This function generate alert dialog for empty message by passing message and
               associated viewcontroller for that function
               - Parameters:
                - message: message that require for  empty alert message
                - viewController: selected viewcontroller at that time
             */
            static func EmptyMessage(message:String, viewController:UITableViewController) {
                let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: viewController.view.bounds.size.width, height: viewController.view.bounds.size.height))
                messageLabel.text = message
                let bubbleColor = UIColor(red: CGFloat(57)/255, green: CGFloat(81)/255, blue: CGFloat(104)/255, alpha :1)
    
                messageLabel.textColor = bubbleColor
                messageLabel.numberOfLines = 0;
                messageLabel.textAlignment = .center;
                messageLabel.font = UIFont(name: "TrebuchetMS", size: 18)
                messageLabel.sizeToFit()
    
                viewController.tableView.backgroundView = messageLabel;
                viewController.tableView.separatorStyle = .none;
            }
    
    0 讨论(0)
  • 2020-12-04 06:09

    So for a safer solution:

    extension UITableView {
    func setEmptyMessage(_ message: String) {
        guard self.numberOfRows() == 0 else {
            return
        }
        let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
        messageLabel.text = message
        messageLabel.textColor = .black
        messageLabel.numberOfLines = 0;
        messageLabel.textAlignment = .center;
        messageLabel.font = UIFont.systemFont(ofSize: 14.0, weight: UIFontWeightMedium)
        messageLabel.sizeToFit()
    
        self.backgroundView = messageLabel;
        self.separatorStyle = .none;
    }
    
    func restore() {
        self.backgroundView = nil
        self.separatorStyle = .singleLine
    }
    
    public func numberOfRows() -> Int {
        var section = 0
        var rowCount = 0
        while section < numberOfSections {
            rowCount += numberOfRows(inSection: section)
            section += 1
        }
        return rowCount
      }
    }
    

    and for UICollectionView as well:

    extension UICollectionView {
    func setEmptyMessage(_ message: String) {
        guard self.numberOfItems() == 0 else {
            return
        }
    
        let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
        messageLabel.text = message
        messageLabel.textColor = .black
        messageLabel.numberOfLines = 0;
        messageLabel.textAlignment = .center;
        messageLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFontWeightSemibold)
        messageLabel.sizeToFit()
        self.backgroundView = messageLabel;
    }
    
    func restore() {
        self.backgroundView = nil
    }
    
    public func numberOfItems() -> Int {
        var section = 0
        var itemsCount = 0
        while section < self.numberOfSections {
            itemsCount += numberOfItems(inSection: section)
            section += 1
        }
        return itemsCount
      }
    }
    

    More Generic Solution:

        protocol EmptyMessageViewType {
          mutating func setEmptyMessage(_ message: String)
          mutating func restore()
        }
    
        protocol ListViewType: EmptyMessageViewType where Self: UIView {
          var backgroundView: UIView? { get set }
        }
    
        extension UITableView: ListViewType {}
        extension UICollectionView: ListViewType {}
    
        extension ListViewType {
          mutating func setEmptyMessage(_ message: String) {
            let messageLabel = UILabel(frame: CGRect(x: 0,
                                                     y: 0,
                                                     width: self.bounds.size.width,
                                                     height: self.bounds.size.height))
            messageLabel.text = message
            messageLabel.textColor = .black
            messageLabel.numberOfLines = 0
            messageLabel.textAlignment = .center
            messageLabel.font = UIFont(name: "TrebuchetMS", size: 16)
            messageLabel.sizeToFit()
    
            backgroundView = messageLabel
        }
    
         mutating func restore() {
            backgroundView = nil
         }
    }
    
    0 讨论(0)
提交回复
热议问题