List Report Floorplan (Tasks 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 and UINavigationBar 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:

  1. Configure the FUISearchController, and set FUISearchBar to UITableView tableHeaderView subview property

    class 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
        }
    
  2. Create an isFiltered flag, and filteredTasks data source to use while filtering on search bar input

    class 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
        }
    
  3. Implement UIKit UISearchResultsUpdating protocol, to respond to changes to the UISearchBar 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 to Render 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)]

}