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:
In this sample, we will:
- Bind the model to views in the controller
- Customize the
UIKit
properties of an Object Table View Cell - Bind Contact Action controls to communication stub methods
- Configure section header views
- 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
}
}