List Report Floorplan (Tools Example)
The list report floorplan is one flat list that contains objects of the same object type. It may contain one or more groups. The user can view each object and take actions from the list report—for instance, grouping them. The user may filter to create a separate list.
Here is an sample of how to implement the example in the Tasks example:
In this sample, we will:
- Bind the model to views in the controller
- Set up Search functionality
- Add
UIToolbar
andUINavigationBar
bar button items
Bind the model to views in the controller
The most common UITableViewCell
utilized in List Reports is the FUIObjectTableViewCell
. Set up an array of Task
objects, and bind that data to a dequeued FUIObjectTableViewCell
.
Be sure to set the tableView.rowHeight = UITableViewAutomaticDimension
, so that the AutoLayout system will correctly calulate the height of the table view cells from their content.
class TasksListReportFloorplanExample: UITableViewController {
var tasks: [ListReportFPData.Task] = ListReportFPData.tasks // see sample model data below
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Active Tasks (\(self.tasks.count))"
self.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
self.tableView.estimatedRowHeight = 120
self.tableView.rowHeight = UITableViewAutomaticDimension
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let task = tasks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier,
for: indexPath) as! FUIObjectTableViewCell
cell.headlineText = task.title
cell.subheadlineText = task.statusSummary
cell.detailImage = task.primaryImage
cell.detailImageView.isCircular = true
cell.footnoteText = {
switch task.onTime {
case Int.min..<0:
return "Overdue by \(abs(task.onTime)) day" + (abs(task.onTime) > 1 ? "s" : "")
case 0:
return "due today"
default:
return "Due in \(task.onTime) day" + (task.onTime > 1 ? "s" : "")
}
}()
cell.statusImage = UIImage(named: "StatusPic")
cell.statusImageView.tintColor = UIColor.preferredFioriColor(forStyle: .negative)
cell.accessoryType = .disclosureIndicator
return cell
}
}
Depending on how your navigation hierarchy is configured, you might notice that your Navigation Bar does not display. If this is the case, ensure that a UINavigationController is active, and controlling the presentation of your view controller. If you have set up a Single-Page Application, you should select your view controller in your storyboard, and select Editor > Embed In > Navigation Controller. For more details, see Apple Documentation.
Set up Search functionality
Use the SAPFiori framework subclasses UISearchController
and UISearchBar
, to natively integrate bar code scanning into the search bar. You may use the FUISearchBar
without barcode scanning, also, as shown below:
Configure the
FUISearchController
, and setFUISearchBar
toUITableView
tableHeaderView
subview propertyclass TasksListReportFloorplanExample: UITableViewController { var searchController: FUISearchController! var tasks: [ListReportFPData.Task] = ListReportFPData.tasks // see sample model data below override func viewDidLoad() { super.viewDidLoad() self.title = "Tasks" self.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier) searchController = FUISearchController(searchResultsController: nil) searchController.searchResultsUpdater = self self.tableView.tableHeaderView = searchController.searchBar self.tableView.estimatedRowHeight = 120 self.tableView.rowHeight = UITableViewAutomaticDimension }
Create an
isFiltered
flag, andfilteredTasks
data source to use while filtering on search bar inputclass TasksListReportFloorplanExample: UITableViewController { var searchController: FUISearchController! var tasks: [ListReportFPData.Task] = ListReportFPData.tasks // see sample model data below var filteredTasks: [ListReportFPData.Task] = [] var isFiltered: Bool = false override func viewDidLoad() { super.viewDidLoad() self.title = "Tasks" self.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier) searchController = FUISearchController(searchResultsController: nil) searchController.searchResultsUpdater = self self.tableView.tableHeaderView = searchController.searchBar self.tableView.estimatedRowHeight = 120 self.tableView.rowHeight = UITableViewAutomaticDimension } // MARK: - Table view data source override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return isFiltered ? filteredTasks.count : tasks.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let task = isFiltered ? filteredTasks[indexPath.row] : tasks[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier, for: indexPath) as! FUIObjectTableViewCell cell.headlineText = task.title cell.subheadlineText = task.statusSummary cell.detailImage = task.primaryImage cell.detailImageView.isCircular = true cell.footnoteText = { switch task.onTime { case Int.min..<0: return "Overdue by \(abs(task.onTime)) day" + (abs(task.onTime) > 1 ? "s" : "") case 0: return "due today" default: return "Due in \(task.onTime) day" + (task.onTime > 1 ? "s" : "") } }() cell.statusImage = UIImage(named: "StatusPic") cell.statusImageView.tintColor = UIColor.preferredFioriColor(forStyle: .negative) cell.accessoryType = .disclosureIndicator return cell }
Implement UIKit
UISearchResultsUpdating
protocol, to respond to changes to theUISearchBar
input// MARK: UISearchResultsUpdating func updateSearchResults(for searchController: UISearchController) { guard let searchString = searchController.searchBar.text, !searchString.isEmpty else { self.isFiltered = false self.filteredTasks.removeAll() self.tableView.reloadData() return } self.isFiltered = true self.filteredTasks = self.tasks.filter( { return $0.title.contains(searchString) }) self.tableView.reloadData() }
Add UIToolbar
and UINavigationBar
bar button items
The following is the finished version of the sample:
class TasksListReportFloorplanExample: UITableViewController, UISearchResultsUpdating {
var searchController: FUISearchController!
var tasks: [TasksListReportFPData.Task] = TasksListReportFPData.tasks
var filteredTasks: [TasksListReportFPData.Task] = []
var isFiltered: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Tasks"
self.filteredTasks = tasks
self.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
searchController = FUISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
self.tableView.tableHeaderView = searchController.searchBar
self.tableView.estimatedRowHeight = 120
self.tableView.rowHeight = UITableViewAutomaticDimension
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(setEditMode))
let addItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addTask))
let filterItem = UIBarButtonItem(title: "Filter", style: .plain, target: self, action: #selector(filterTasks))
let spacing = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
self.navigationController?.isToolbarHidden = false
self.toolbarItems = [filterItem, spacing, addItem]
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isFiltered ? filteredTasks.count : tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let task = isFiltered ? filteredTasks[indexPath.row] : tasks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier, for: indexPath) as! FUIObjectTableViewCell
cell.headlineText = task.title
cell.subheadlineText = task.statusSummary
cell.detailImage = task.primaryImage
cell.detailImageView.isCircular = true
cell.footnoteText = {
switch task.onTime {
case Int.min..<0:
return "Overdue by \(abs(task.onTime)) day" + (abs(task.onTime) > 1 ? "s" : "")
case 0:
return "due today"
default:
return "Due in \(task.onTime) day" + (task.onTime > 1 ? "s" : "")
}
}()
cell.statusImage = UIImage(named: "StatusPic")
cell.statusImageView.tintColor = UIColor.preferredFioriColor(forStyle: .negative)
cell.accessoryType = .disclosureIndicator
return cell
}
// MARK: UISearchResultsUpdating
func updateSearchResults(for searchController: UISearchController) {
guard let searchString = searchController.searchBar.text, !searchString.isEmpty else {
self.isFiltered = false
self.filteredTasks.removeAll()
self.tableView.reloadData()
return
}
self.isFiltered = true
self.filteredTasks = self.tasks.filter( { return $0.title.contains(searchString) })
self.tableView.reloadData()
}
// MARK: Handler for UIToolbar Button Item Actions
func addTask() {
// add task here
}
func filterTasks() {
// set filter mode here
}
func setEditMode() {
self.tableView.setEditing(!self.tableView.isEditing, animated: true)
}
}
Sample Data
Before running the application, add two images to your project’s Asset catalog, with the names
"ProfilePic"
, and"StatusPic"
. For more information on working with Asset catalogs, see Apple documentation. The"StatusPic"
image should be set toRender as: Template
, to allow the tint color to be set at runtime. The image should have a transparent background, to serve as a template. Apple documentation.
struct TasksListReportFPData {
enum TaskStatus {
case normal
case high
}
struct Task {
let title: String
let statusSummary: String
let onTime: Int
let primaryImage: UIImage?
let secondaryImage: UIImage?
let status: TaskStatus
}
static let tasks: [Task] = [Task(title: "Decision Needed about Part Time Employment", statusSummary: "Issue not yet resolved", onTime: -2, primaryImage: UIImage(named: "ProfilePic"), secondaryImage: nil, status: .high),
Task(title: "Estimation for Part Time Employment", statusSummary: "Issue has been published", onTime: -2, primaryImage: UIImage(named: "ProfilePic"), secondaryImage: nil, status: .high ),
Task(title: "Documentation for Environment", statusSummary: "Progress is at 95%", onTime: 0, primaryImage: UIImage(named: "ProfilePic"), secondaryImage: nil, status: .high),
Task(title: "Define Server Configuration", statusSummary: "Task might delay milestone", onTime: 3, primaryImage: UIImage(named: "ProfilePic"), secondaryImage: nil, status: .high),
Task(title: "Catalog Backend System is Slow", statusSummary: "Task progress seems low", onTime: 4, primaryImage: UIImage(named: "ProfilePic"), secondaryImage: nil, status: .high)]
}