web-dev-qa-db-fra.com

Gestion d'un UITableView vide. Imprimer un message amical

J'ai un UITableView que dans certains cas, il est légal d'être vide. Donc, au lieu d'afficher l'image de fond de l'application, je préférerais imprimer un message convivial à l'écran, tel que:

Cette liste est maintenant vide

Quel est le moyen le plus simple de le faire?

104
cateof

La propriété backgroundView de UITableView est votre ami.

Dans viewDidLoad ou n'importe où vous reloadData, vous devez déterminer si votre table est vide ou non et mettre à jour la propriété backgroundView de UITableView avec un UIView contenant un UILabel ou simplement le définir sur nil. C'est ça.

Il est bien sûr possible de faire en sorte que la source de données de UITableView fasse un double travail et renvoie une cellule spéciale "la liste est vide", cela me semble être un kludge. Soudain, numberOfRowsInSection:(NSInteger)section doit calculer le nombre de lignes des autres sections sur lesquelles il n'a pas été interrogé pour s'assurer qu'elles sont également vides. Vous devez également créer une cellule spéciale contenant le message vide. N'oubliez pas non plus que vous devez probablement modifier la hauteur de votre cellule pour prendre en charge le message vide. Tout cela est faisable, mais cela ressemble à du pansement par-dessus le pansement.

156
Ray Fix

Sur la base des réponses fournies ici, voici un cours rapide que j'ai créé et que vous pouvez utiliser dans votre UITableViewController.

import Foundation
import UIKit

class TableViewHelper {

    class func EmptyMessage(message:String, viewController:UITableViewController) {
        let rect = CGRect(Origin: CGPoint(x: 0,y :0), size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height))
        let messageLabel = UILabel(frame: rect)
        messageLabel.text = message
        messageLabel.textColor = UIColor.blackColor()
        messageLabel.numberOfLines = 0;
        messageLabel.textAlignment = .Center;
        messageLabel.font = UIFont(name: "TrebuchetMS", size: 15)
        messageLabel.sizeToFit()

        viewController.tableView.backgroundView = messageLabel;
        viewController.tableView.separatorStyle = .None;
    }
}

Dans votre UITableViewController, vous pouvez appeler cela dans numberOfSectionsInTableView(tableView: UITableView) -> Int

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    if projects.count > 0 {
        return 1
    } else {
        TableViewHelper.EmptyMessage("You don't have any projects yet.\nYou can create up to 10.", viewController: self)
        return 0
    }
}

enter image description here

Avec un peu d'aide de http://www.appcoda.com/pull-to-refresh-uitableview-empty/

80
Johnston

Identique à la réponse de Jhonston, mais je l’ai préférée comme extension:

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
}
53
Frankie

Je recommande la bibliothèque suivante: DZNEmptyDataSet

Le moyen le plus simple de l’ajouter à votre projet est de l’utiliser avec des Cocaopod comme ceci: pod 'DZNEmptyDataSet'

Dans votre TableViewController, ajoutez l'instruction d'importation suivante (Swift):

import DZNEmptyDataSet

Ensuite, assurez-vous que votre classe est conforme à DNZEmptyDataSetSource et DZNEmptyDataSetDelegate comme ceci:

class MyTableViewController: UITableViewController, DZNEmptyDataSetSource, DZNEmptyDataSetDelegate

Dans votre viewDidLoad ajoutez les lignes de code suivantes:

tableView.emptyDataSetSource = self
tableView.emptyDataSetDelegate = self
tableView.tableFooterView = UIView()

Maintenant, tout ce que vous avez à faire pour montrer le vidage est:

//Add title for empty dataset
func titleForEmptyDataSet(scrollView: UIScrollView!) -> NSAttributedString! {
    let str = "Welcome"
    let attrs = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)]
    return NSAttributedString(string: str, attributes: attrs)
}

//Add description/subtitle on empty dataset
func descriptionForEmptyDataSet(scrollView: UIScrollView!) -> NSAttributedString! {
    let str = "Tap the button below to add your first grokkleglob."
    let attrs = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
    return NSAttributedString(string: str, attributes: attrs)
}

//Add your image
func imageForEmptyDataSet(scrollView: UIScrollView!) -> UIImage! {
    return UIImage(named: "MYIMAGE")
}

//Add your button 
func buttonTitleForEmptyDataSet(scrollView: UIScrollView!, forState state: UIControlState) -> NSAttributedString! {
    let str = "Add Grokkleglob"
    let attrs = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleCallout)]
    return NSAttributedString(string: str, attributes: attrs)
}

//Add action for button
func emptyDataSetDidTapButton(scrollView: UIScrollView!) {
    let ac = UIAlertController(title: "Button tapped!", message: nil, preferredStyle: .Alert)
    ac.addAction(UIAlertAction(title: "Hurray", style: .Default, handler: nil))
    presentViewController(ac, animated: true, completion: nil)
}

Ces méthodes ne sont pas obligatoires, il est également possible d'afficher l'état vide sans bouton, etc.

Pour Swift 4

// MARK: - Deal with the empty data set
// Add title for empty dataset
func title(forEmptyDataSet _: UIScrollView!) -> NSAttributedString! {
    let str = "Welcome"
    let attrs = [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)]
    return NSAttributedString(string: str, attributes: attrs)
}

// Add description/subtitle on empty dataset
func description(forEmptyDataSet _: UIScrollView!) -> NSAttributedString! {
    let str = "Tap the button below to add your first grokkleglob."
    let attrs = [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)]
    return NSAttributedString(string: str, attributes: attrs)
}

// Add your image
func image(forEmptyDataSet _: UIScrollView!) -> UIImage! {
    return UIImage(named: "MYIMAGE")
}

// Add your button
func buttonTitle(forEmptyDataSet _: UIScrollView!, for _: UIControlState) -> NSAttributedString! {
    let str = "Add Grokkleglob"
    let attrs = [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: UIFontTextStyle.callout), NSAttributedStringKey.foregroundColor: UIColor.white]
    return NSAttributedString(string: str, attributes: attrs)
}

// Add action for button
func emptyDataSetDidTapButton(_: UIScrollView!) {
    let ac = UIAlertController(title: "Button tapped!", message: nil, preferredStyle: .alert)
    ac.addAction(UIAlertAction(title: "Hurray", style: .default, handler: nil))
    present(ac, animated: true, completion: nil)
}
14
Hapeki

Une façon de le faire serait de modifier votre source de données pour renvoyer 1 lorsque le nombre de lignes est égal à zéro et pour produire une cellule à usage spécifique (avec éventuellement un identificateur de cellule différent) dans la tableView:cellForRowAtIndexPath: méthode.

-(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
    ...
}

Cela peut devenir quelque peu compliqué si vous devez gérer plusieurs contrôleurs de vue de table, car quelqu'un finira par oublier d'insérer un contrôle zéro. Une meilleure approche consiste à créer une implémentation séparée d'une implémentation UITableViewDataSource qui renvoie toujours une seule ligne avec un message configurable (appelons-le EmptyTableViewDataSource). Lorsque les données gérées par votre contrôleur d'affichage de table changent, le code qui gère la modification vérifie si les données sont vides. S'il n'est pas vide, définissez votre contrôleur d'affichage de table avec sa source de données standard; sinon, définissez-le avec une instance de EmptyTableViewDataSource qui a été configurée avec le message approprié.

12
dasblinkenlight

J'ai utilisé le message titleForFooterInSection pour cela. Je ne sais pas si c'est sous-optimal ou non, mais ça marche.

-(NSString*)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section   {

    NSString *message = @"";
    NSInteger numberOfRowsInSection = [self tableView:self.tableView numberOfRowsInSection:section ];

    if (numberOfRowsInSection == 0) {
        message = @"This list is now empty";
    }

    return message;
}
10
Carlos B

Donc, pour une solution plus sûre:

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
  }
}

et pour UICollectionView aussi:

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
  }
}

Solution plus générique:

    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
     }
}
7
Meseery

L'utilisation de backgroundView convient, mais elle ne défile pas correctement, comme dans Mail.app.

J'ai fait quelque chose de similaire à ce que xtravar a fait.

J'ai ajouté une vue en dehors de la hiérarchie de vues du tableViewController. hierarchy

Ensuite, j'ai utilisé le code suivant dans tableView:numberOfRowsInSection::

if someArray.count == 0 {
    // Show Empty State View
    self.tableView.addSubview(self.emptyStateView)
    self.emptyStateView.center = self.view.center
    self.emptyStateView.center.y -= 60 // rough calculation here
    self.tableView.separatorColor = UIColor.clear
} else if self.emptyStateView.superview != nil {
    // Empty State View is currently visible, but shouldn't
    self.emptyStateView.removeFromSuperview()
    self.tableView.separatorColor = nil
}

return someArray.count

Fondamentalement, j'ai ajouté le emptyStateView en tant que sous-vue de l'objet tableView. Comme les séparateurs chevaucheraient la vue, j'ai défini leur couleur sur clearColor. Pour revenir à la couleur de séparation par défaut, vous pouvez simplement la définir sur nil.

5
mangerlahn

Je ne peux que recommander de glisser-déposer une UITextView dans la TableView après les cellules. Établissez une connexion avec ViewController et masquez-la/affichez-la le cas échéant (par exemple, chaque fois que la table est rechargée).

enter image description here

5
pasql

Utiliser un contrôleur de vue de conteneur est la bonne façon de procéder selon Apple .

J'ai mis toutes mes vues d'état vides dans un Storyboard séparé. Chacune de ses sous-classes UIViewController. J'ajoute du contenu directement sous leur vue racine. Si une action/un bouton est nécessaire, vous avez déjà un contrôleur pour le gérer.
Ensuite, il suffit d'instancier le contrôleur de vue souhaité à partir de ce Storyboard, de l'ajouter en tant que contrôleur de vue enfant et d'ajouter la vue du conteneur à la hiérarchie du tableau (vue secondaire). Votre vue d'état vide sera également défilable, ce qui sera agréable et vous permettra de mettre en œuvre l'extraction pour l'actualisation.

Lire chapitre 'Ajout d'un contrôleur de vue enfant à votre contenu' pour obtenir de l'aide sur la mise en œuvre.

Assurez-vous simplement que vous définissez le cadre de vue enfant sur (0, 0, tableView.frame.width, tableView.frame.height) et les choses seront centrées et alignées correctement.

4
AmitP

Premièrement, les problèmes avec d’autres approches populaires.

BackgroundView

La vue en arrière-plan ne serait pas bien centrée si vous utilisiez le simple cas de le définir pour qu'il soit un UILabel.

Cellules, en-têtes ou pieds de page pour afficher le message

Cela interfère avec votre code fonctionnel et introduit des cas étranges Edge. Si vous voulez centrer parfaitement votre message, cela ajoute un autre niveau de complexité.

Rouler votre propre contrôleur de vue de table

Vous perdez des fonctionnalités intégrées, telles que refreshControl, et réinventez la roue. Adhérez à UITableViewController pour obtenir les meilleurs résultats possibles.

Ajout de UITableViewController en tant que contrôleur de vue enfant

J'ai l'impression que vous allez vous retrouver avec des problèmes de contentInset dans iOS 7+ - et pourquoi compliquer les choses?

Ma solution

La meilleure solution que j'ai proposée (et, d'accord, ce n'est pas idéal) est de créer une vue spéciale pouvant s'asseoir sur une vue de défilement et agir en conséquence. Cela se complique évidemment dans iOS 7 avec la folie contentInset, mais c'est faisable.

Choses que vous devez surveiller:

  • les séparateurs de table sont mis en avant à un moment donné pendant reloadData - vous devez vous protéger contre cela
  • contentInset/contentOffset - observez ces touches sur votre vue spéciale
  • clavier - si vous ne voulez pas que le clavier gêne, c'est un autre calcul
  • autolayout - vous ne pouvez pas compter sur les changements de cadre pour positionner votre vue

Une fois que vous avez compris cela une fois dans une sous-classe UIView, vous pouvez l’utiliser pour tout: charger les fileurs, désactiver les vues, afficher les messages d’erreur, etc.

3
xtravar

C'est la solution la meilleure et la plus simple.

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 60)];
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 60)];
    label.text = @"This list is empty";
    label.center = self.view.center;
    label.textAlignment = NSTextAlignmentCenter;
    [view addSubview:label];

    self.tableView.backgroundView = view;
2
Rajesh

Afficher le message pour une liste vide, que ce soit ITableView ou ICollectionView.

extension UIScrollView {
    func showEmptyListMessage(_ message:String) {
        let rect = CGRect(Origin: CGPoint(x: 0,y :0), size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height))
        let messageLabel = UILabel(frame: rect)
        messageLabel.text = message
        messageLabel.textColor = .black
        messageLabel.numberOfLines = 0
        messageLabel.textAlignment = .center
        messageLabel.font = UIFont.systemFont(ofSize: 15)
        messageLabel.sizeToFit()

        if let `self` = self as? UITableView {
            self.backgroundView = messageLabel
            self.separatorStyle = .none
        } else if let `self` = self as? UICollectionView {
            self.backgroundView = messageLabel
        }
    }
}

sages:

if cellsViewModels.count == 0 {
    self.tableView.showEmptyListMessage("No Product In List!")
}

OR:

if cellsViewModels.count == 0 {
    self.collectionView?.showEmptyListMessage("No Product In List!")
}

Rappelez-vous: N'oubliez pas de supprimer l'étiquette du message au cas où les données viendraient après l'actualisation.

2
Gurjit Singh

Sélectionnez votre scène tableviewController dans le storyboard

enter image description here

Glissez et déposez UIView Add label avec votre message (par exemple: Aucune donnée)

enter image description here

créez un débouché d’UIView (par exemple, yournoDataView) sur votre TableViewController.

et dans viewDidLoad

self.tableView.backgroundView = yourNoDataView

1
gaurav bhardwaj

Utilisation de Swift 4.2

  func numberOfSections(in tableView: UITableView) -> Int
{
    var numOfSections: Int = 0
    if self.medArray.count > 0
    {
        tableView.separatorStyle = .singleLine
        numOfSections            = 1
        tableView.backgroundView = nil
    }
    else
    {
        let noDataLabel: UILabel  = UILabel(frame: CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: tableView.bounds.size.height))
        noDataLabel.text          = "No  Medicine available.Press + to add New Pills "
        noDataLabel.textColor     = UIColor.black
        noDataLabel.textAlignment = .center
        tableView.backgroundView  = noDataLabel
        tableView.separatorStyle  = .none
    }
    return numOfSections
}
1
M Murteza

Ce n’est probablement pas la meilleure solution, mais j’ai fait cela en mettant simplement une étiquette au bas de mon tableau et si les lignes = 0, je lui ai assigné du texte. Assez facile, et réalise ce que vous essayez de faire avec quelques lignes de code.

J'ai deux sections dans mon tableau (emplois et écoles)

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    if (jobs.count == 0 && schools.count == 0) {
        emptyLbl.text = "No jobs or schools"
    } else {
        emptyLbl.text = ""
    }
0
Zach

J'ai fait quelques modifications pour que nous n'ayons pas besoin de vérifier le nombre manuellement, mais j'ai également ajouté des contraintes pour l'étiquette afin que rien ne se passe mal, quelle que soit la taille du message, comme indiqué ci-dessous:

extension UITableView {

    fileprivate func configureLabelLayout(_ messageLabel: UILabel) {
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        let labelTop: CGFloat = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 25:15)
        messageLabel.topAnchor.constraint(equalTo: backgroundView?.topAnchor ?? NSLayoutAnchor(), constant: labelTop).isActive = true
        messageLabel.widthAnchor.constraint(equalTo: backgroundView?.widthAnchor ?? NSLayoutAnchor(), constant: -20).isActive = true
        messageLabel.centerXAnchor.constraint(equalTo: backgroundView?.centerXAnchor ?? NSLayoutAnchor(), constant: 0).isActive = true
    }

    fileprivate func configureLabel(_ message: String) {
        let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
        messageLabel.textColor = .black
        messageLabel.numberOfLines = 0
        messageLabel.textAlignment = .center
        let fontSize = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 25:15)
        let font: UIFont = UIFont(name: "MyriadPro-Regular", size: fontSize) ?? UIFont()
        messageLabel.font = font
        messageLabel.text = message
        self.backgroundView = UIView()
        self.backgroundView?.addSubview(messageLabel)
        configureLabelLayout(messageLabel)
        self.separatorStyle = .none
    }

    func setEmptyMessage(_ message: String, _ isEmpty: Bool) {
        if isEmpty { // instead of making the check in every TableView DataSource in the project
            configureLabel(message)
        }
        else {
            restore()
        }

    }

    func restore() {
        self.backgroundView = nil
        self.separatorStyle = .singleLine
    }
}

tilisation

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let message: String = "The list is empty."
        ticketsTableView.setEmptyMessage(message, tickets.isEmpty)
        return self.tickets.count
    }
0
Alsh compiler

version Swift mais une forme meilleure et plus simple. ** 3.

J'espère que cela servira votre but ......

Dans votre 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
        }
    }
}

Classe d'assistance avec fonction:

 /* 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
Ravindra Shekhawat

Pour ce faire, le moyen le plus simple et le plus rapide consiste à faire glisser une étiquette sur le panneau latéral sous tableView. Créez un point de vente pour l’étiquette et la table et ajoutez une instruction if pour masquer et afficher l’étiquette et la table selon vos besoins. Vous pouvez également ajouter tableView.tableFooterView = UIView (frame: CGRect.zero), ce qui vous le permettra viewDidLoad () pour donner à un tableau vide la perception qu'il est masqué si le tableau et la vue d'arrière-plan ont la même couleur.

0
yoamod