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
Based on the answers here, here is a quick class I made that you can use on in your 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;
}
}
In your UITableViewController
you can call this in 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
}
}
With a little help from http://www.appcoda.com/pull-to-refresh-uitableview-empty/
This is the best and simple solution.
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;
First, the problems with other popular approaches.
BackgroundView
Background view doesn't center nicely if you were to use the simple case of setting it to be a UILabel.
Cells, headers, or footers to display the message
This interferes with your functional code and introduces weird edge cases. If you want to perfectly center your message, that adds another level of complexity.
Rolling your own table view controller
You lose built-in functionality, such as refreshControl, and re-invent the wheel. Stick to UITableViewController for the best maintainable results.
Adding UITableViewController as a child view controller
I have a feeling you'll end up with contentInset issues in iOS 7+ - plus why complicate things?
My solution
The best solution I've come up with (and, granted, this isn't ideal) is to make a special view that can sit on top of a scroll view and act accordingly. This obviously gets complicated in iOS 7 with contentInset madness, but it's doable.
Things you have to watch out for:
Once you have this figured out once in one UIView subclass, you can use it for everything - loading spinners, disabling views, showing error messages, etc.
I have been using the titleForFooterInSection message for this. I don't know if this is suboptimal or not, but it works.
-(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;
}
You can add this to your Base class.
var messageLabel = UILabel()
func showNoDataMessage(msg: String) {
let rect = CGRect(origin: CGPoint(x: 0, y :self.view.center.y), size: CGSize(width: self.view.bounds.width - 16, height: 50.0))
messageLabel = UILabel(frame: rect)
messageLabel.center = self.view.center
messageLabel.text = msg
messageLabel.numberOfLines = 0
messageLabel.textColor = Colors.grayText
messageLabel.textAlignment = .center;
messageLabel.font = UIFont(name: "Lato-Regular", size: 17)
self.view.addSubview(messageLabel)
self.view.bringSubviewToFront(messageLabel)
}
Show it like this in the class on getting the data from api.
func populateData(dataSource : [PRNJobDataSource]){
self.dataSource = dataSource
self.tblView.reloadData()
if self.dataSource.count == 0 {self.showNoDataMessage(msg: "No data found.")}
}
Hide it like this.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.dataSource.count > 0 {self.hideNoDataMessage()}
return dataSource.count
}
func hideNoDataMessage(){
messageLabel.removeFromSuperview()
}
Using a Container View Controller is the right way to do it according to Apple.
I put all my empty state views in a separate Storyboard. Each under it's own UIViewController subclass. I add content directly under their root view. If any action/button is needed, you now already have a controller to handle it.
Then its just a matter of instantiating the desired view controller from that Storyboard, add it as a child view controller and add the container view to the tableView's hierarchy (sub view).
Your empty state view will be scrollable as well, which feels good and allow you to implement pull to refresh.
Read chapter 'Adding a Child View Controller to Your Content' for help on how to implement.
Just make sure you set the child view frame as
(0, 0, tableView.frame.width, tableView.frame.height)
and things will be centered and aligned properly.