Tratando fontes de dados vazias com DZNEmptyDataSet

Tratando fontes de dados vazias com DZNEmptyDataSet

Grande parte dos projetos desenvolvidos no iOS tem um UITableView ou um UICollectionView. Há situações em que a fonte de dados relacionadas a estes componentes não retornam nenhum dado. Seja porque o usuário ainda não cadastrou nenhum informação, seja por uma pesquisa que não retornou resultado ou por uma conexão a internet que não pode ser concretizada. Nestes casos a interface do usuário fica com uma tela vazia que pode confundir o usuário nos passos que ele deve proceder. Vamos criar um projeto que demonstre esta situação e depois iremos utilizar o DZEmptyDataSet para criar uma interface mais amigável para os usuários.

Criando um novo projeto

Crie um novo projeto Single View Application no Xcode para iPhone utilizando Swift.

Navegue até o diretório do projeto e inicialize o CocoaPods com o comando pod init.

Edite o arquivo Podfile para adicionar o pod DZEmptyDataSet conforme o exemplo a seguir:

# Uncomment this line to define a global platform for your project
platform :ios, '9.0'

target 'DZNEmptyDataSetExample' do
  # Comment this line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for DZNEmptyDataSetExample
  pod 'DZNEmptyDataSet'
end

Execute o comando pod install para instalar o pod. Feche o projeto no Xcode e abra o workspace criado pelo CocoaPods. Se tiver dúvidas sobre a instalação de pods verifique o post sobre o CocoaPods.

Neste tutorial irei focar no DZEmptyDataSet. Criei um simples ToDo List para utilizarmos como exemplo. A nossa classe ViewController possui dois Outlets. O primeiro é um UITableView e o segundo é um UISearchBar. Temos dois DataSources. O toDoList será utilizado para carregar e salvar a lista de pendências no disco e o filteredToDoList será utilizado como DataSource do tableView.
No método viewDidLoad() atribuímos o nosso controller como delegate do searchBar e do tableView e como o dataSource do tableView.
Carregamos a lista de pendências do disco e criamos os botões para adicionar uma nova pendência e excluir todas as pendências.
O método filterData() é o método principal do controller ele é o responsável por filtrar as pendências do toDoList utilizando NSPredicate de acordo com os filtros selecionados pelo usuário. Também utilizamos o NSSortDescriptor para ordenar as pendências por finalização e por ordem alfabética. Este ordenação é fundamental para agrupar as pendências no tableView.
O método selectItem(_:) é utilizado para encontrar a pendência no array toDoList de acordo com a pendência selecionada no array filteredToDoList.
Segue o fonte da classe ViewController que iremos utilizar.

import UIKit

class ViewController: UIViewController {
  // MARK: - Outlets
  @IBOutlet weak var tableView: UITableView!
  @IBOutlet weak var searchBar: UISearchBar!

  // MARK: Properties
  let cellName = "ToDoItemCell"

  var toDoList: [ToDoItem]!
  var filteredToDoList = [ToDoItem]()

  // MARK: Methods
  override func viewDidLoad() {
    super.viewDidLoad()

    tableView.dataSource = self
    tableView.delegate = self

    searchBar.delegate = self

    loadToDoList()

    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: #selector(addItemAction))
    navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Trash, target: self, action: #selector(deleteAllItemsAction))

    title = NSLocalizedString("ToDoList.title", value: "To Do List", comment: "")
  }

  func addItemAction() {
    let alert = UIAlertController(title: NSLocalizedString("ToDoList.Add.title", value: "New To Do Item", comment: ""),
                                  message: nil,
                                  preferredStyle: .Alert)
    alert.addTextFieldWithConfigurationHandler(nil)

    alert.addAction(UIAlertAction(title: NSLocalizedString("ToDoList.Add.Cancel", value: "Cancel", comment: ""), style: .Cancel, handler: nil))

    alert.addAction(UIAlertAction(title: NSLocalizedString("ToDoList.Add.OK", value: "Ok", comment: ""), style: .Default) { [unowned self, alert] _ in
      let item = ToDoItem(item: alert.textFields![0].text!, finalized: false)
      self.addItem(item)
    })

    alert.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem

    presentViewController(alert, animated: true, completion: nil)
  }

  func deleteAllItemsAction() {
    toDoList.removeAll()

    saveToDoList()
  }

  // MARK: Private Methods
  private func filterData() {
    if let text = searchBar.text {
      let finalizedSort = NSSortDescriptor(key: "finalized", ascending: true)
      let itemSort = NSSortDescriptor(key: "item", ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))

      if text.characters.count == 0 {
        filteredToDoList = (toDoList as NSArray).sortedArrayUsingDescriptors([finalizedSort, itemSort]) as! [ToDoItem]
      } else {
        let search = NSPredicate(format: "item CONTAINS[cd] %@", text)
        filteredToDoList = (toDoList as NSArray).filteredArrayUsingPredicate(search) as! [ToDoItem]

        filteredToDoList = (filteredToDoList as NSArray).sortedArrayUsingDescriptors([finalizedSort, itemSort]) as! [ToDoItem]
      }

      if searchBar.selectedScopeButtonIndex == 0 {
        let search = NSPredicate(format: "finalized = %@", false)
        filteredToDoList = (filteredToDoList as NSArray).filteredArrayUsingPredicate(search) as! [ToDoItem]
      } else if searchBar.selectedScopeButtonIndex == 1 {
        let search = NSPredicate(format: "finalized = %@", true)
        filteredToDoList = (filteredToDoList as NSArray).filteredArrayUsingPredicate(search) as! [ToDoItem]
      }

      tableView.reloadData()
    }
  }

  private func saveToDoList() {
    let savedData = NSKeyedArchiver.archivedDataWithRootObject(toDoList)
    let defaults = NSUserDefaults.standardUserDefaults()

    defaults.setObject(savedData, forKey: "ToDoList")

    filterData()
  }

  private func loadToDoList() {
    let defaults = NSUserDefaults.standardUserDefaults()

    if let savedPeople = defaults.objectForKey("ToDoList") as? NSData {
      toDoList = NSKeyedUnarchiver.unarchiveObjectWithData(savedPeople) as! [ToDoItem]
    } else {
      toDoList = [ToDoItem]()
    }

    filterData()
  }

  private func addItem(item: ToDoItem) {
    toDoList.append(item)

    saveToDoList()
  }

  private func selectItem(indexPath: NSIndexPath) -> ToDoItem {
    if indexPath.section == 0 {
      return filteredToDoList[indexPath.row]
    } else {
      let pendingItems = filteredToDoList.filter { !$0.finalized }

      return filteredToDoList[indexPath.row + pendingItems.count]
    }
  }
}

Implementando o protocolo UITableViewDataSource

No método numberOfSectionsInTableView(_:) iremos retornar dois quando o UISegmentedControl do UISearchBar for igual a 2 (Todas) e retornar um quando o segmento selecionado for 0 (Pendentes) ou 1 (Finalizadas).
No método tableView(_:,numberOfRowsInSection:) iremos utilizar um cálculo para descobrir o verdadeiro valor do índice do array. Como nosso array tem uma dimensão mas o tableView espera duas seções separadas, iremos somar o valor do índice quando for a seção 1 com a quantidade de pendências da seção 0. Este cálculo só é necessário quando o segmento selecionado for 2 (Todas).
No método tableView(_:,titleForHeaderInSection:) iremos retornar Pendentes quando a propriedade finalized do ToDoItem for falso ou retornar Finalizadas quando for verdadeiro.
No método tableView(_:,cellForRowAtIndexPath:) iremos utilizar as funções selectItem(_:) para retornar o item utilizando o indexPath e depois chamar a função fillCell() da classe ToDoItemTableViewCell que iremos criar adiante.

//MARK: -
extension ViewController: UITableViewDataSource {
  // MARK: - UITableViewDataSource
  func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    if searchBar.selectedScopeButtonIndex == 2 {
      return 2
    }

    return 1
  }

  func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if searchBar.selectedScopeButtonIndex == 2 {
      let result: [ToDoItem]

      if section == 0 {
        result = filteredToDoList.filter { !$0.finalized }
      } else {
        result = filteredToDoList.filter { $0.finalized }
      }

      return result.count
    }

    return filteredToDoList.count
  }

  func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    if searchBar.selectedScopeButtonIndex == 2 {
      if section == 0 {
        return NSLocalizedString("ToDoList.Table.Header.Pending", value: "Pending", comment: "")
      }

      return NSLocalizedString("ToDoList.Table.Header.Completed", value: "Completed", comment: "")
    }

    return nil
  }

  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(cellName) as! ToDoItemTableViewCell
    let item = selectItem(indexPath)

    cell.fillCell(item)

    return cell
  }
}

Implementando o protocolo UITableViewDelegate

O método tableView(_:,didSelectRowAtIndexPath:) passa o índice da célula selecionada para a função updateItem(). A função updateItem() utiliza a função selectItem(_:) vista anteriomente para encontrar a pendência selecionada. Descobrimos o índice da pendência no array toDoList e atualizamos a pendência com a propriedade finalized invertida. Depois salvamos o array chamando o método SaveToDoList().
Os métodos tableView(_:,heightForRowAtIndexPath:) e tableView(_:,estimatedHeightForRowAtIndexPath:) são utilizados para que a altura da célula seja calculada dinamicamente de acordo com o Auto Layout configurado no Main.storyboard.

//MARK: -
extension ViewController: UITableViewDelegate {
  // MARK: - UITableViewDelegate
  func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    updateItem(indexPath)
  }

  func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
  }

  func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
  }

  private func updateItem(indexPath: NSIndexPath) {
    let item = selectItem(indexPath)

    if let index = toDoList.indexOf(item) {
      item.finalized = !item.finalized

      toDoList[index] = item

      saveToDoList()
    }
  }
}

Implementando o protocolo UISearchBarDelegate

Os métodos searchBar(_:,selectedScopeButtonIndexDidChange:) e searchBar(_:,textDidChange:) chamam a função filterData() quando o segmentedControl ou o textBox do searchBar são alterados.
O método searchBarSearchButtonClicked(_:) esconde o teclado quando o usuário clica no botão pesquisar.

//MARK: -
extension ViewController: UISearchBarDelegate {
  // MARK: - UISearchBarDelegate
  func searchBar(searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
    filterData()
  }

  func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
    filterData()
  }

  func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    searchBar.resignFirstResponder()
  }
}

Criando o model

O nosso model será a classe ToDoItem. A classe possui apenas duas propriedades o nome da pendência (item) do tipo string e um boleado para indicar se ela já foi finalizada (finalized). Implementei o protocolo NSCoding para salvar a lista de pendências com o NSUserDefaults Segue o fonte da classe.

import UIKit

class ToDoItem: NSObject, NSCoding {
  var item: String
  var finalized: Bool

  required init(item: String, finalized: Bool = false) {
    self.item = item
    self.finalized = finalized

    super.init()
  }

  required init?(coder aDecoder: NSCoder) {
    self.item = aDecoder.decodeObjectForKey("Item") as? String ?? ""
    self.finalized = aDecoder.decodeBoolForKey("Finalized") ?? false
  }

  func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(item, forKey: "Item")
    aCoder.encodeBool(finalized, forKey: "Finalized")
  }
}

Criando uma UITableViewCell

Por último temos a classe ToDoItemTableViewCell que será a célula da nossa tabela. Esta classe possui dois Outlets para o label e para o botão de status. A classe também possui dois métodos. O primeiro (prepareForReuse()) serve para preparar a célula para ser reutilizada e o segundo (fillCell(_:)) serve para configurar os dados de acordo com o ToDoItem recebido. Segue o fonte da classe.

import UIKit

class ToDoItemTableViewCell: UITableViewCell {
  @IBOutlet weak var itemLabel: UILabel!
  @IBOutlet weak var checkedButton: UIButton!

  override func prepareForReuse() {
    itemLabel.text = ""
    checkedButton.selected = false
  }

  func fillCell(item: ToDoItem) {
    itemLabel.text = item.item
    checkedButton.selected = item.finalized
  }
}

Desenhando a interface

Interface ToDo List

Nossa interface é bastante simples. Temos um UITableView, um UISearchBar e um UITableViewCell com um UILabel e um UIButton.

Document Outline

Altere a classe do UITableViewCell para ToDoItemTableViewCell. Marque a opção Shows Scope Bar e adicione as opções Pending (Pendentes), Completed (Finalizadas) e All (Todas).

UISearchBar

Adicione a imagem button-done-normal para o State Config Default e button-done-selected para o State Config Selected.

button-done-normal button-done-normal@2x button-done-selected button-done-selected@2x

Rodando o app

ToDo List ToDo List ToDo List

Utilizando o DZNEmptyDataSet

Quando a nossa lista está vazia ou quando realizamos uma pesquisa que não retorna nenhum registro o tableView fica mostrando apenas as linhas que separam as células. Com isto o usuário não sabe se o filtro selecionado não retorna resultado ou se a lista está realmente vazia. Iremos utilizar a biblioteca DZNEmptyDataSet para apresentar uma informação mais amigável para o usuário e ajudá-lo a entender melhor qual é o status da lista de pendências.

Adicione a bilbioteca ao arquivo ViewController.

import DZNEmptyDataSet

No método viewDidLoad configure as propriedades emptyDataSetDelegate e tableFooterView do tableView.

tableView.emptyDataSetSource = self
tableView.emptyDataSetDelegate = self

Adicione o comando abaixo para retirar as linhas do tableView quando o dataSource não retornar dados.

// A little trick for removing the cell separators
tableView.tableFooterView = UIView()

A biblioteca DZNEmptyDataSet possui dois protocolos. O DZNEmptyDataSetSource e o DZNEmptyDataSetDelegate.

O protocolo DZNEmptyDataSetSource possui os seguintes métodos:

  1. titleForEmptyDataSet(_:)
  2. descriptionForEmptyDataSet(_:)
  3. imageForEmptyDataSet(_:)
  4. imageTintColorForEmptyDataSet(_:)
  5. imageAnimationForEmptyDataSet(_:)
  6. buttonTitleForEmptyDataSet(_:,forState:)
  7. buttonImageForEmptyDataSet(_:,forState:)
  8. buttonBackgroundImageForEmptyDataSet(_:,forState:)
  9. backgroundColorForEmptyDataSet(_:)
  10. customViewForEmptyDataSet(_:)
  11. offsetForEmptyDataSet(_:)
  12. spaceHeightForEmptyDataSet(_:)

O protocolo DZNEmptyDataSetDelegate possui os seguintes métodos:

  1. emptyDataSetShouldFadeIn(_:)
  2. emptyDataSetShouldBeForcedToDisplay(_:)
  3. emptyDataSetShouldDisplay(_:)
  4. emptyDataSetShouldAllowTouch(_:)
  5. emptyDataSetShouldAllowScroll(_:)
  6. emptyDataSetShouldAnimateImageView(_:)
  7. emptyDataSetDidTapView(_:)
  8. emptyDataSetDidTapButton(_:)
  9. emptyDataSet(_:,didTapView:)
  10. emptyDataSet(_:,didTapButton:)
  11. emptyDataSetWillAppear(_:)
  12. emptyDataSetDidAppear(_:)
  13. emptyDataSetWillDisappear(_:)
  14. emptyDataSetDidDisappear

Consulte sobre os detalhes dos métodos disponíveis na página do projeto no GitHub.

Implementando o protocolo DZNEmptyDataSetSource

Para melhorar a usabilidade da nossa lista de pendências iremos mostrar um título e uma descrição informando ao usuário o motivo de não haver pendências. Quando for o caso da lista estar vazia também iremos mostrar uma imagem e um botão para que o usuário possa cadastrar uma nova pendência.

Para apresentar o título utilizaremos o método titleForEmptyDataSet(_:). Ele espera que seja retornado um NSAttributedString.
Caso a lista esteja vazia iremos apresentar a mensagem “Cadastre sua primeira pendência” senão, iremos apresentar a mensagem “Pendência não encontrada”. Formatamos a mensagem para que ela fique centralizada, tenha a cor cinza e o tamanho UIFontTextStyleTitle1.

//MARK: -
extension ViewController: DZNEmptyDataSetSource {
  // MARK: - DZNEmptyDataSetSource
  func titleForEmptyDataSet(scrollView: UIScrollView!) -> NSAttributedString! {
    let text: String

    if toDoList.isEmpty {
      text = NSLocalizedString("ToDoList.EmptyDataSet.Title", value: "Add your first task", comment: "")
    } else {
      text = NSLocalizedString("ToDoList.EmptyDataSet.Search.Title", value: "Task not found", comment: "")
    }

    let paragraph = NSMutableParagraphStyle()
    paragraph.lineBreakMode = .ByWordWrapping
    paragraph.alignment = .Center

    let attributes = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleTitle1), NSForegroundColorAttributeName: UIColor.grayColor(), NSParagraphStyleAttributeName: paragraph]

    let attibutedString = NSMutableAttributedString(string: text as String, attributes: attributes)

    return attibutedString
  }
}

Para mostrarmos o texto iremos utilizar o método descriptionForEmptyDataSet(_:). Ele também espera um NSAttributedString como retorno.
Neste caso temos algumas opções que podem acontecer:

  • A lista está vazia e a mensagem será “Clique no botão abaixo para criar uma nova pendência.”;
  • Houve uma pesquisa por um texto específico e a mensagem será “Nenhuma pendência com a descrição %@ encontrada.”;
  • Não há pendências com o status “Pendente” e a mensagem será “Nenhuma pendência com o status Pendente foi encontrada.”;
  • Não há pendências com o status “Finalizada” e a mensagem será “Nenhuma pendência com o status Finalizada foi encontrada.”

Utilizamos a fonte UIFontTextStyleBody na cor cinza claro e com alinhamento centralizado. Depois destacamos o texto pesquisado se houver pesquisa ou o status especificado quando for o caso.

func descriptionForEmptyDataSet(scrollView: UIScrollView!) -> NSAttributedString! {
  let text: NSString
  var status = ""

  if toDoList.isEmpty {
    text = NSLocalizedString("ToDoList.EmptyDataSet.Description", value: "Click on the button below to create a new task.", comment: "")
  } else if let search = searchBar.text where search.characters.count > 0 {
    text = String(format: NSLocalizedString("ToDoList.EmptyDataSet.Search.Description", value: "No task with description %@ found.", comment: ""), search)
  } else {
    switch searchBar.selectedScopeButtonIndex {
      case 0:
        status = NSLocalizedString("ToDoList.EmptyDataSet.Status.Pending", value: "Pending", comment: "")
        text = String(format: NSLocalizedString("ToDoList.EmptyDataSet.Filter.Description", value: "No task with status %@ was found.", comment: ""),  status)
      case 1:
        status = NSLocalizedString("ToDoList.EmptyDataSet.Status.Completed", value: "Completed", comment: "")
        text = String(format: NSLocalizedString("ToDoList.EmptyDataSet.Filter.Description", value: "No task with status %@ was found.", comment: ""),  status)
      default:
        text = ""
    }
  }

  let paragraph = NSMutableParagraphStyle()
  paragraph.lineBreakMode = .ByWordWrapping
  paragraph.alignment = .Center

  let attributes = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleBody), NSForegroundColorAttributeName: UIColor.lightGrayColor(), NSParagraphStyleAttributeName: paragraph]

  let attibutedString = NSMutableAttributedString(string: text as String, attributes: attributes)

  if !toDoList.isEmpty && searchBar.text!.characters.count > 0 {
    attibutedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.darkGrayColor(), range: text.rangeOfString(searchBar.text!))
  } else if !toDoList.isEmpty {
    attibutedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.darkGrayColor(), range: text.rangeOfString(status))
  }

  return attibutedString
}

Utilzamos o método imageForEmptyDataSet(_:) para retornar uma imagem quando a lista estiver vazia.

ToDoList ToDoList ToDoList

Imagem de Zlatko Najdenovski.

func imageForEmptyDataSet(scrollView: UIScrollView!) -> UIImage! {
  if toDoList.isEmpty {
    if let image = UIImage(named: "ToDoList") {
      return image
    }
  }

  return nil
}

Mudamos a cor da imagem para cinza claro com o método imageTintColorForEmptyDataSet(_:).

func imageTintColorForEmptyDataSet(scrollView: UIScrollView!) -> UIColor! {
  return UIColor.lightGrayColor()
}

Criamos um botão com o texto “Nova Pendência” quando a lista estiver vazia através do método buttonTitleForEmptyDataSet(_:,forState:). Como o meódo espera um NSAttributedString como retorno aproveitamos para alterar a fonte para a cor branca com o tamanho UIFontTextStyleBody.

func buttonTitleForEmptyDataSet(scrollView: UIScrollView!, forState state: UIControlState) -> NSAttributedString! {
  if toDoList.isEmpty {
    let text: String

    text = NSLocalizedString("ToDoList.EmptyDataSet.ButtonTitle", value: "New Task", comment: "")

    let attributes = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleBody), NSForegroundColorAttributeName: UIColor.whiteColor()]

    let attibutedString = NSMutableAttributedString(string: text as String, attributes: attributes)

    return attibutedString
  }

  return nil
}

Para destacar o texo branco do botão incluimos uma imagem de fundo. Para isto utilizamos o método buttonBackgroundImageForEmptyDataSet(_:,forState:).

ButtonBackground ButtonBackground ButtonBackground

func buttonBackgroundImageForEmptyDataSet(scrollView: UIScrollView!, forState state: UIControlState) -> UIImage! {
  if toDoList.isEmpty {
    if let image = UIImage(named: "ButtonBackground") {
      return image
    }
  }

  return nil
}

Pra finalizar alteramos o espaço entre a imagem, o título, a descrição e o botão para ficar melhor apresentável. Pra isto utilizamos o método spaceHeightForEmptyDataSet(_:).

func spaceHeightForEmptyDataSet(scrollView: UIScrollView!) -> CGFloat {
  return 20.0
}

Quando a lista está vazia e o filtro está em “Todas” os cabeçalhos do tableView são exibidos na tela. Iremos alterar o método tableView(_:,titleForHeaderInSection:) para não mostrar os cabeçalhos do tableView quando não houverem pedências.

func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  if !toDoList.isEmpty && searchBar.selectedScopeButtonIndex == 2 {
    if section == 0 {
      return NSLocalizedString("ToDoList.Table.Header.Pending", value: "Pending", comment: "")
    }

    return NSLocalizedString("ToDoList.Table.Header.Completed", value: "Completed", comment: "")
  }

  return nil
}

Implementando o protocolo DZNEmptyDataSetDelegate

Iremos utilizar o método emptyDataSet(_:,didTapButton:) para definir o evento do botão que é exibido quando a lista está vazia. Para isto iremos chamar o método addItemAction() definido anteriormente.

//MARK: -
extension ViewController: DZNEmptyDataSetDelegate {
  // MARK: - DZNEmptyDataSetDelegate
  func emptyDataSet(scrollView: UIScrollView!, didTapButton button: UIButton!) {
    addItemAction()
  }
}

Conclusão

Com isto finalizamos a nossa lista de pendências. Veja como ficou.

Sem pendências

Uma pendência Uma pendência Uma pendência

Uma pendência concluída Uma pendência concluída Uma pendência concluída

Duas pendências Duas pendências Duas pendências

Como vimos o tratamento de fonte de dados vazia faz uma grande diferença na usabilidade do aplicativo. A biblioteca DZNEmptyDataSet simplifica o trabalho de tratar esta situação com os protocolos e os métodos oferecidos.

Espero que tenham gostado do artigo. Caso tenham alguma dúvida, crítica ou sugestão deixe um comentário abaixo ou no twitter @mateusfsilva. Caso tenham gostado por favor compartilhem.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s