Skip to content

Object Detail Floorplan

The object details floorplan functions as the child screen of an object floorplan. It is used to show additional information about one attribute of a business object on the object floorplan, and it is accessed by drill-down. The object details screen is usually the last screen in a single workflow. However, the object details floorplan can include actions and modal views to display further details.

Here is a sample of how one would implement an object details floorplan, preceding the modal navigation example:

image

In this sample, we will:

  1. Bind the model to views in the controller
  2. Customize the UIKit properties of an Object Table View Cell
  3. Bind Contact Action controls to communication stub methods
  4. Configure section header views
  5. Implement a collection view section in the table view

Bind the Model to Views in the Controller

Sections 0 and 1 of this table view controller are populated by FUIObjectTableViewCell and FUIContactCell table view cells, respectively. The FUIObjectTableViewCell is particularly interesting, as its specification in this app screen requires tweaking the default configuration of the cell. To configure the changes, use the UIKit APIs of its subviews.

Be sure to set the tableView.rowHeight = UITableViewAutomaticDimension, so that the AutoLayout system will correctly calculate the height of the table view cells from their content.

import SAPFiori

class ObjectDetailsFloorplanExample: UITableViewController, UICollectionViewDataSource {

    let contacts = ObjectDetailsFPDataSource.contacts

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
        self.tableView.register(FUIContactCell.self, forCellReuseIdentifier: FUIContactCell.reuseIdentifier)

        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 120
    }

    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 1:
            return 3
        default:
            return 1
        }
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        switch indexPath.section {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier, for: indexPath) as! FUIObjectTableViewCell
            cell.headlineText = "Lock out energy-isolating devices with an assigned individual lock."
            cell.footnoteText = "Notify all affected personnel and customers that a lockout is required on this equipment, and that it will be out of service during maintenance.  Provide the reason for the service disruption."

            // override the `numberOfLines` property in the cell labels, to enable wrapping ("0" == wrapping)
            cell.headlineLabel.numberOfLines = 0
            cell.footnoteLabel.numberOfLines = 0

            cell.statusText = "Safety"
            cell.statusLabel.textColor = UIColor.preferredFioriColor(forStyle: .negative)

            // set icon images - NOTE:  a max of 2 icons will be displayed, if only headline & footnote labels are populated
            cell.iconImages = ["3", UIImage(named: "checkmark")!, UIImage(named: "attachment")!]

            // force the cell to layout content full-width, even on wide screens
            cell.isApplyingSplitPercent = false

            return cell
        default:
            let contact = contacts[indexPath.row]
            let cell = tableView.dequeueReusableCell(withIdentifier: FUIContactCell.reuseIdentifier, for: indexPath) as! FUIContactCell
            cell.detailImage = contact.image
            cell.headlineText = contact.name
            cell.subheadlineText = contact.title

            // set activity items to activity control
            cell.activityControl.addActivities( [FUIActivityItem.phone, FUIActivityItem.videoCall, FUIActivityItem.message])

            // handle selection handler for each activity
            cell.onActivitySelectedHandler = { activity in
                switch activity {
                case FUIActivityItem.phone:
                    contact.call()
                case FUIActivityItem.videoCall:
                    contact.video()
                case FUIActivityItem.message:
                    contact.sendMessage()
                default:
                    break
                }
            }
            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, you should 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.

Configure Section Header Views

The SAPFiori framework includes a view for UITableView header and/or footer section views: FUITableViewHeaderFooterView. You should use the UITableViewDelegate tableView(_:viewForHeaderInSection:) method to supply section header(s) to the table view (and the footer variant for footer views).

You should generally use UITableViewAutomaticDimension to set section (and/or footer) view heights. However, you can supply this constant to the UITableViewDelegate tableView(_:heightForHeaderInSection:) method, to mix static and AutoLayout-calculated heights.

class ObjectDetailsFloorplanExample: UITableViewController, UICollectionViewDataSource {

    var attachmentsSection: FUITableViewCollectionSection!  

    let contacts = ObjectDetailsFPDataSource.contacts
    let attachments = ObjectDetailsFPDataSource.attachments

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
        self.tableView.register(FUIContactCell.self, forCellReuseIdentifier: FUIContactCell.reuseIdentifier)
        self.tableView.register(FUITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: FUITableViewHeaderFooterView.reuseIdentifier)

        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 120

        // set estimated header height
        self.tableView.estimatedSectionHeaderHeight = 44
    }

    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 1:
            return 3
        default:
            return 1
        }
    }

    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: FUITableViewHeaderFooterView.reuseIdentifier) as! FUITableViewHeaderFooterView

        switch section {
        case 1:
            view.titleLabel.text = "Contacts"
        case 2:
            view.titleLabel.text = "Related Attachments"
        default:
            return nil
        }
        return view
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        switch indexPath.section {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier, for: indexPath) as! FUIObjectTableViewCell
            cell.headlineText = "Lock out energy-isolating devices with an assigned individual lock."
            cell.footnoteText = "Notify all affected personnel and customers that a lockout is required on this equipment, and that it will be out of service during maintenance.  Provide the reason for the service disruption."

            // override the `numberOfLines` property in the cell labels, to enable wrapping ("0" == wrapping)
            cell.headlineLabel.numberOfLines = 0
            cell.footnoteLabel.numberOfLines = 0

            cell.statusText = "Safety"
            cell.statusLabel.textColor = UIColor.preferredFioriColor(forStyle: .negative)

            // set icon images - NOTE:  a max of 2 icons will be displayed, if only headline & footnote labels are populated
            cell.iconImages = ["3", UIImage(named: "checkmark")!, UIImage(named: "attachment")!]

            // force the cell to layout content full-width, even on wide screens
            cell.isApplyingSplitPercent = false

            return cell
        default:
            let contact = contacts[indexPath.row]
            let cell = tableView.dequeueReusableCell(withIdentifier: FUIContactCell.reuseIdentifier, for: indexPath) as! FUIContactCell
            cell.detailImage = contact.image
            cell.headlineText = contact.name
            cell.subheadlineText = contact.title

            // set activity items to activity control
            cell.activityControl.addActivities( [FUIActivityItem.phone, FUIActivityItem.videoCall, FUIActivityItem.message])

            // handle selection handler for each activity
            cell.onActivitySelectedHandler = { activity in
                switch activity {
                case FUIActivityItem.phone:
                    contact.call()
                case FUIActivityItem.videoCall:
                    contact.video()
                case FUIActivityItem.message:
                    contact.sendMessage()
                default:
                    break
                }
            }
            return cell
        }
    }

    // supply fixed very small height to eliminate header spacing for first section; use UITableViewAutomaticDimension for others
    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        guard section > 0 else {
            return CGFloat.leastNonzeroMagnitude
        }
        return UITableViewAutomaticDimension
    }

Implement a Collection View Section in the Table View

class ObjectDetailsFloorplanExample: UITableViewController, UICollectionViewDataSource {

    // Use a FUITableViewCollectionSection, for resizing UICollectionView in UITableViewCell
    var attachmentsSection: FUITableViewCollectionSection!

    let contacts = ObjectDetailsFPDataSource.contacts
    let attachments = ObjectDetailsFPDataSource.attachments

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
        self.tableView.register(FUIContactCell.self, forCellReuseIdentifier: FUIContactCell.reuseIdentifier)
        self.tableView.register(FUITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: FUITableViewHeaderFooterView.reuseIdentifier)

        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 120

        // set estimated header height
        self.tableView.estimatedSectionHeaderHeight = 44

        // Use the FUICollectionViewLayout.horizontalFlowLayout with minimumScaledItemSize, to scale collection view items to content width
        let attachmentsLayout = FUICollectionViewLayout.horizontalFlow
        attachmentsLayout.minimumScaledItemSize = CGSize(width: 100, height: 100)
        attachmentsLayout.numberOfColumns = 4

        self.attachmentsSection = FUITableViewCollectionSection(tableView: self.tableView, collectionViewLayout: attachmentsLayout)
        self.attachmentsSection.collectionView.register(MyThumbnailCollectionViewCell.self, forCellWithReuseIdentifier: MyThumbnailCollectionViewCell.reuseIdentifier)

        // Set UICollectionViewDataSource of section's collectionView
        self.attachmentsSection.collectionView.dataSource = self
        self.attachmentsSection.collectionView.isScrollEnabled = false
    }

    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 3
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 1:
            return 3
        default:
            return 1
        }
    }

    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: FUITableViewHeaderFooterView.reuseIdentifier) as! FUITableViewHeaderFooterView

        switch section {
        case 1:
            view.titleLabel.text = "Contacts"
        case 2:
            view.titleLabel.text = "Related Attachments"
        default:
            return nil
        }
        return view
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        switch indexPath.section {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier, for: indexPath) as! FUIObjectTableViewCell
            cell.headlineText = "Lock out energy-isolating devices with an assigned individual lock."
            cell.footnoteText = "Notify all affected personnel and customers that a lockout is required on this equipment, and that it will be out of service during maintenance.  Provide the reason for the service disruption."

            // override the `numberOfLines` property in the cell labels, to enable wrapping ("0" == wrapping)
            cell.headlineLabel.numberOfLines = 0
            cell.footnoteLabel.numberOfLines = 0

            cell.statusText = "Safety"
            cell.statusLabel.textColor = UIColor.preferredFioriColor(forStyle: .negative)

            // set icon images - NOTE:  a max of 2 icons will be displayed, if only headline & footnote labels are populated
            cell.iconImages = ["3", UIImage(named: "checkmark")!, UIImage(named: "attachment")!]

            // force the cell to layout content full-width, even on wide screens
            cell.isApplyingSplitPercent = false

            return cell
        case 1:
            let contact = contacts[indexPath.row]
            let cell = tableView.dequeueReusableCell(withIdentifier: FUIContactCell.reuseIdentifier, for: indexPath) as! FUIContactCell
            cell.detailImage = contact.image
            cell.headlineText = contact.name
            cell.subheadlineText = contact.title

            // set activity items to activity control
            cell.activityControl.addActivities( [FUIActivityItem.phone, FUIActivityItem.videoCall, FUIActivityItem.message])

            // handle selection handler for each activity
            cell.onActivitySelectedHandler = { activity in
                switch activity {
                case FUIActivityItem.phone:
                    contact.call()
                case FUIActivityItem.videoCall:
                    contact.video()
                case FUIActivityItem.message:
                    contact.sendMessage()
                default:
                    break
                }
            }
            return cell
        default:

            // return collectionViewTableViewCell property of collection section
            return self.attachmentsSection.collectionViewTableViewCell
        }
    }

    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        guard section > 0 else {
            return CGFloat.leastNonzeroMagnitude
        }
        return UITableViewAutomaticDimension
    }

    // MARK:  UICollectionViewDataSource
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return attachments.count
    }

    public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyThumbnailCollectionViewCell.reuseIdentifier, for: indexPath) as! MyThumbnailCollectionViewCell
        let image = UIImage(named: attachments[indexPath.row])
        cell.imageView.image = image

        return cell
    }
}

Sample Data

Before running the application, add an image to your project's Asset catalog, named "ProfilePic", and a set of images to the Asset catalog for your attachment's thumbnails. Name these images to match the names of attachment thumbnail images in the sample data, or update the sample data names accordingly. For more information on working with Asset catalogs, see Apple documentation.

// Contact DataSource mockup
struct ObjectDetailsFPDataSource {

    struct Contact {

        var name: String
        var title: String
        var address: String
        var image: UIImage

        func call() { print( "Making phone call to \(self.name)") }
        func sendMessage() { print( "Sending message to \(self.name)")}
        func video() { print( "Launching FaceTime for \(self.name)") }
    }

    static let contacts: [Contact] = [
        Contact(name: "Alex Kilgo", title: "Team Lead", address: "Rose Hospital Boston\nBoston, MA 02109\n555-323-2826", image: UIImage(named: "ProfilePic")!),
        Contact(name: "Luka Ning", title: "Expert", address: "Rose Hospital Boston\nBoston, MA 02109\n555-323-2826", image: UIImage(named: "ProfilePic")!),
        Contact(name: "Natasha Girotra", title: "Vendor Contact", address: "Rose Hospital Boston\nBoston, MA 02109\n555-323-2826", image: UIImage(named: "ProfilePic")!)]

    // names of attachment thumbnail images
    static let attachments: [String] = ["attachment009.5", "attachment010", "attachment011", "attachment003", "attachment007", "attachment001", "attachment004"]
}

This example uses a custom collection view cell. Copy the following declaration to your application project to use.

class MyThumbnailCollectionViewCell : UICollectionViewCell {
    let imageView: `UIImageView` = UIImageView()

    open static var reuseIdentifier: String {
        return "\(String(describing: self))"
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setup()
    }

    func setup() {
        self.addSubview(self.imageView)
        self.imageView.translatesAutoresizingMaskIntoConstraints = false
        self.imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
        self.imageView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
        self.imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
        self.imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        self.imageView.contentMode = .scaleAspectFill
    }
}

Last update: January 15, 2021