Skip to Content

Create a List Report Floorplan

test
0 %
Create a List Report Floorplan
Details

Create a List Report Floorplan

2020-02-05
Use the SAP Cloud Platform SDK for iOS to build a simple List Report Floorplan containing an FUISearchBar

You will learn

  • How to use the SAPFiori UI controls to build a List Report Floorplan

Prerequisites


Step 1: Make the Generated App Your Own

In this example, you build upon the Tutorial app created using the Sample OData service. If you examine the service’s metadata you can see entity Supplier has a one-to-many relationship with Products:

<EntityType Name="Supplier">
  <Key>
    <PropertyRef Name="SupplierId"/>
  </Key>
  <Property MaxLength="40" Name="City" Nullable="true" Type="Edm.String"/>
  <Property MaxLength="3" Name="Country" Nullable="true" Type="Edm.String"/>
  <Property MaxLength="255" Name="EmailAddress" Nullable="true" Type="Edm.String"/>
  <Property MaxLength="10" Name="HouseNumber" Nullable="true" Type="Edm.String"/>
  <Property MaxLength="30" Name="PhoneNumber" Nullable="true" Type="Edm.String"/>
  <Property MaxLength="10" Name="PostalCode" Nullable="true" Type="Edm.String"/>
  <Property MaxLength="60" Name="Street" Nullable="true" Type="Edm.String"/>
  <Property MaxLength="10" Name="SupplierId" Nullable="false" Type="Edm.String"/>
  <Property MaxLength="80" Name="SupplierName" Nullable="true" Type="Edm.String"/>
  <Property Name="UpdatedTimestamp" Type="Edm.DateTime"/>
  <NavigationProperty FromRole="Supplier" Name="Products" Relationship="ESPM.Supplier_Product_One_Many0" ToRole="Product"/>
</EntityType>

The app you’ve generated with the SAP Cloud Platform SDK for iOS Assistant (iOS Assistant) has currently its UI. You’re going to make that app your own now.

Open the Main.storyboard, select all displayed View Controllers and delete them.

Change UI

Add a new UITableViewController to the storyboard and embed it in a UINavigationController.

Change UI

Now change the UINavigationController to be the Initial View Controller.

Change UI

To let the app load your newly added screen it is necessary to change the application screen code in the ApplicationUIManager.swift.

Open the ApplicationUIManager.swift class and locate the showApplicationScreen(completionHandler:) method.

Change UI

Replace the following code inside the showApplicationScreen(completionHandler:) method:

  let appDelegate = (UIApplication.shared.delegate as! AppDelegate)
  let splitViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "MainSplitViewController") as! UISplitViewController
  splitViewController.delegate = appDelegate
  splitViewController.modalPresentationStyle = .currentContext
  splitViewController.preferredDisplayMode = .allVisible
  appViewController = splitViewController

with:

   let navigationViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController() as! UINavigationController
   appViewController = navigationViewController

Instead of instantiating the MainSplitViewController the UIStoryboard will instantiate the initial View Controller and cast it to the UINavigationController.

Lastly, you have to create a new class inheriting from UITableViewController. Create a new Cocoa Touch Class and name it SupplierTableViewController.

Change UI

Set the created class as Custom Class in the storyboard for the added UITableViewController.

Change UI
Log on to answer question
Step 2: Fetch All Available Suppliers From the Sample Service

Now that you have the first UITableViewController setup you will add code to load and display suppliers in a UITableView.

Open the SupplierTableViewController.swift class and add the following import statements for full usage of the SDK:

import SAPOData
import SAPFoundation
import SAPFiori
import SAPFioriFlows
import SAPCommon

Next let the SupplierTableViewController.swift class conforms to the SAPFioriLoadingIndicator protocol provided by the iOS Assistant generated code. You can locate the protocol in the Utils group.

class SupplierTableViewController: UITableViewController, SAPFioriLoadingIndicator { ... }

Xcode will ask you to fully conform to the protocol by adding a loadingIndicator class property. Add the following line of code directly below the class declaration:

var loadingIndicator: FUILoadingIndicatorView?

The way the iOS Assistant generates the project you need the AppDelegate.swift to retrieve an instance of the generated convenience data service. Implement the following lines of code directly below the loading indicator property:

private let appDelegate = UIApplication.shared.delegate as! AppDelegate

var dataService: ESPMContainer<OnlineODataProvider>? {
  return OnboardingSessionManager.shared.onboardingSession?.odataController.espmContainer ?? nil
}

NOTE: In case you’re using an ODataOfflineProvider you have to change the above-mentioned code to use ODataOfflineProvider instead of ODataOnlineProvider. You have to also import SAPOfflineOData in addition to the SAPOData framework.

Because we’re good citizens we want to use an app logger to log important information. Fortunately, SAP is offering a simple-to-use Logging API with the SAPCommon framework.

Implement the following line of code below the data service declaration:

private let logger = Logger.shared(named: "SupplierTableViewController")

Before you will implement the data loading methods you have to implement a supplier array property to save the loaded data in memory.

Add the following line of code below the logger instantiation:

private var suppliers = [Supplier]()

NOTE: From now on bigger code blocks are explained with inline comments. Read the inline comments carefully to fully understand what the code is doing and why we’re implementing it.

Loading all available suppliers is fairly easy using the generated data service. The generated code will handle all authentication and authorization challenges for you and the data service will construct all necessary requests to load, create and update entities in your backend.

Implement the following methods directly under the closing bracket of the viewDidLoad() method:

// MARK: - Data loading

/// When this method gets called to show a loading indicator. When the completion handler of the loadData(:) method gets executed hide the loading indicator.
private func updateTableView() {
    self.showFioriLoadingIndicator()
    loadData {
        self.hideFioriLoadingIndicator()
    }
}

/// Load the suppliers by using the fetchSuppliers() method provided by the data service.
private func loadData(completionHandler: @escaping () -> Void) {
    // fetch products
    dataService?.fetchSuppliers() { [weak self] suppliers, error in
        // handle errors
        if let error = error {
            self?.logger.error("Error while fetching list of suppliers.", error: error)
            return
        }

        // set loaded suppliers to property and reload data on the table view
        self?.suppliers = suppliers!
        self?.tableView.reloadData()
        completionHandler()
    }
}

Call the updateTableView() method inside the viewDidLoad():

override func viewDidLoad() {
    super.viewDidLoad()

    updateTableView()
}

Congratulations, your app is fetching data now.

Log on to answer question
Step 3: Implement the Table View to display a supplier list

Using the UITableViewController makes it easy for a developer to display data in a list.

First, implement two necessary UITableViewDataSource methods which are responsible for telling the UITableView how many sections and rows to display. Implement those two methods directly below the loadData(:) method:

// MARK: - Table view data source

/// We are only displaying one section for this screen, return 1.
override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

/// Return the count of the suppliers array.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return suppliers.count
}

Using the SAPFiori framework allows you to choose from a large variety of UITableViewCell classes.
Because we’re going to display suppliers, and those have a name, an address and probably contact data the FUIContactCell would be a perfect fit here. You can always use the SAP Fiori Mentor app, available in the App Store for iPad, to get an introduction to the control.

Before implementing the tableView(_cellForRowAt:) method responsible for dequeuing reusable cells and returning them to the UITableView, we want to register the FUIContactCell with the UITableView first. This is usually done in the viewDidLoad() method.

Implement the following lines of code before the updateTableView() method call in the viewDidLoad():

// Register the cell with the provided convenience reuse identifier.
tableView.register(FUIContactCell.self, forCellReuseIdentifier: FUIContactCell.reuseIdentifier)

// Set the seperator style of the table view to none and the background color to the standard Fiori background base color.
tableView.separatorStyle = .none
tableView.backgroundColor = .preferredFioriColor(forStyle: .backgroundBase)

It is time to implement the tableView(_cellForRowAt:) method to dequeue the FUIContactCell:

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

  // Get the specific supplier for the current row.
  let supplier = suppliers[indexPath.row]

  // Dequeue the FUIContactCell using the convenience reuse identifier. Force cast it to the FUIContactCell
  let cell = tableView.dequeueReusableCell(withIdentifier: FUIContactCell.reuseIdentifier) as! FUIContactCell

  // Set values to the cell's properties.
  cell.headlineText = supplier.supplierName ?? "No Name available!"
  cell.subheadlineText = "\(supplier.street ?? "") \(supplier.houseNumber ?? "") \(supplier.city ?? ""), \(supplier.postalCode ?? "") \(supplier.country ?? "")"

  // Because we're going to implement navigation later this cell has the disclosure indicator as accessory type indicating that navigation to the user.
  cell.accessoryType = .disclosureIndicator

  return cell
}

You could run the app now and should see a list of suppliers getting loaded and displayed.

Log on to answer question
Step 4: Add an FUIActivityControl to your FUIContactCell

Using the FUIContactCell allows us to use an FUIActivityControl inside of the cell to let the user contact a supplier. The FUIActivityControl documentation explains the control and its variations in more detail.

First, you have to define which so-called FUIActivityItems you want to use in the FUIActivityControl.

Add the following line of code below the supplier’s array:

/// Define the contact possibilities for the user: messaging, phone call, and email
private let activities = [FUIActivityItem.message, FUIActivityItem.phone, FUIActivityItem.email]

With that, you can simply add those activities to the FUIContactCell. This specific cell carries the FUIActivityControl in its belly in is accessible through the available public API.

Go back to the tableView(_cellForRowAt:) method and add the following lines of code directly above the assignment of the accessory type:

cell.activityControl.addActivities(activities)
cell.activityControl.maxVisibleItems = 3

// The FUIActivityControl provides you two different ways of reacting to user's interaction. One would be with a change handler the other would be with a delegate. Because I don't want the communication logic being in the tableView(_cellForRowAt:) method we're using the delegation way.
cell.activityControl.delegate = self

To properly react to the user’s interaction with the control you will implement an extension conforming to the FUIActivityControlDelegate.

Implement the extension:

extension SupplierTableViewController: FUIActivityControlDelegate {

  func activityControl(_ activityControl: FUIActivityControl, didSelectActivity activityItem: FUIActivityItem) {

    // Use a Switch to check for the identifier and act accordingly.
    switch activityItem.identifier {
    case FUIActivityItem.message.identifier:
        AlertHelper.displayAlert(with: "Messaging supplier!", error: nil, viewController: self)
        break
    case FUIActivityItem.phone.identifier:
        AlertHelper.displayAlert(with: "Calling supplier!", error: nil, viewController: self)
        break
    case FUIActivityItem.email.identifier:
        AlertHelper.displayAlert(with: "Send email to supplier!", error: nil, viewController: self)
        break
    default:
        return
    }
  }
}

In this tutorial, you will not implement the actual code for doing the communication. If you’re interested in how to do so, you can use Apple’s official documentation:

If you run the app now you should see the following screen:

Supplier List

Tapping on one of the FUIActivityItem will result in an alert dialogue showing up.

Supplier List
Log on to answer question
Step 5: Implement the Navigation Between the Supplier List and the Product List

In this step, we will implement a second UITableViewController displaying all products a supplier provides.
For this, we will use a storyboard segue to navigate to the SupplierProductsTableViewController and pass through the selected supplier.

NOTE: In case you’re not familiar with segues please visit, and carefully read the official documentation before continuing. Using Segues

Open up the Main.storyboard and add a new UITableViewController from the Object Library directly next to the SupplierTableViewController. Create a new segue from one of the prototype cells inside of the SupplierTableViewController to the newly added UITableViewController.

Segue

Select the segue and open the Identity Inspector to set the Identifier to showSupplierProducts.

Segue

Go back to the SupplierTableViewController and add a new class constant of type String which will hold the segue identifier.

private let segueIdentifier = "showSupplierProducts"

Create a new Cocoa Touch class with the name SupplierProductsTableViewController.

Segue

Set the Custom Class of the newly added UITableViewController to SupplierProductsTableViewController in the Main.storyboard.

Segue

Open the SupplierProductsTableViewController and add a class property that will contain the selected supplier.

var supplier: Supplier!

Next, we will implement the prepare(:for:sender:) method which is responsible for making necessary preparations before the navigation is fully executed. In our case, we will pass the selected supplier to the SupplierProductsTableViewController.

Implement the prepare(:for:sender:) method:

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

  // Check for the correct identifier
  if segue.identifier == segueIdentifier {

    // Get the selected row from the table view.
    if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {

      // Get the destination view controller and cast it to SupplierProductsTableViewController.
      let destinationVC = segue.destination as! SupplierProductsTableViewController

      // Set the selected supplier.
      destinationVC.supplier = suppliers[indexPathForSelectedRow.row]
    }
  }
}

You can now navigate back and forth between the SupplierTableViewController and the SupplierProductsTableViewController.

Log on to answer question
Step 6: Implement the loading and displaying of supplier-specific products

This view is similar to the SupplierTableViewController but instead of fetching all products, we will fetch supplier-specific products. To achieve that, we again can utilize the OData APIs. SAPOData provides the possibility to create so-called DataQuery objects which can define typical OData arguments for a backend call.

First, we need to make the needed import statements for that class:

import SAPOData
import SAPFoundation
import SAPFiori
import SAPFioriFlows
import SAPCommon

Let’s use the logger again. Add the following line of code below the supplier property:

private let logger = Logger.shared(named: "SupplierProductsViewController")

Add a couple of class properties necessary for the data service instance and the fetched products. Implement the following lines of code:

private let appDelegate = UIApplication.shared.delegate as! AppDelegate

var dataService: ESPMContainer<OnlineODataProvider>? {
    return OnboardingSessionManager.shared.onboardingSession?.odataController.espmContainer ?? nil
}

private var products = [Product]()

Also, we want to utilize the provided loading indicator. Let the class conform to the SAPFioriLoadingIndicator protocol.

class SupplierProductsTableViewController: UITableViewController, SAPFioriLoadingIndicator { ... }

Of course, don’t forget the loading indicator class property:

var loadingIndicator: FUILoadingIndicatorView?

Let’s load some data!

We’re using the same style we’ve used in the SupplierTableViewController. Implement the following two methods and read the inline comments carefully because you will see that we utilize the DataQuery object for making a filter as well as an expand.

NOTE: If you’re not familiar with those OData specific terms please make yourself familiar with the OData specification:
- URI Conventions (OData Version 2.0)
- OData Version 4.01. Part 2: URL Conventions


// You know that one u{1F609} private func updateTableView() { self.showFioriLoadingIndicator() loadData { self.hideFioriLoadingIndicator() } } private func loadData(completionHandler: @escaping () -> Void) { // Retrieve the supplier id let supplierID = supplier.supplierID! // Create a new DataQuery object applying a filter and an expand. The filter will filter on the specific // supplier id and the expand will construct the URL in a way that the backend returns the stock details // for each product. let dataQuery = DataQuery().filter(Product.supplierID == supplierID).expand(Product.stockDetails) dataService?.fetchProducts(matching: dataQuery) { [weak self] products, error in // handle errors if let error = error { self?.logger.error("Error while fetching list of products for supplier: \(supplierID).", error: error) return } // set loaded products to property and reload data on the table view self?.products = products! self?.tableView.reloadData() completionHandler() } }

Call the updateTableView() method as last statement in the viewDidLoad().

override func viewDidLoad() {
    super.viewDidLoad()

    updateTableView()
}

We know that the products contain images for each product, it would be nice to display them as well, but to do so we have to write a little bit of code to make that happen.

First, implement a class property holding the image URLs of all products.

private var productImageURLs = [String]()

The user might want to scroll through the products even if the images are not fully loaded yet we have to implement a simple image cache as well as a placeholder image to keep the performance of the table stable.

Add the following line of code directly below the productImageURLs:

// This simple dictionary will contain all the fetched images in memory.
private var imageCache = [String: UIImage]()

Now implement a method responsible for fetching the product images and caching them. Add the following method right below the loadData(:) method:


/// This method will take a URL and an escaping completion handler as arguments. The URL is the backend URL for your deployed sample service. private func loadProductImageFrom(_ url: URL, completionHandler: @escaping (_ image: UIImage) -> Void) { // Retrieve the SAP URLSession from the onboarding session. if let sapURLSession = appDelegate.sessionManager.onboardingSession?.sapURLSession { // Create a data task, this is the same as the URLSession data task. sapURLSession.dataTask(with: url, completionHandler: { data, _, error in // Handle errors if let error = error { self.logger.error("Failed to load image!", error: error) return } // Instantiate an image from data. if let image = UIImage(data: data!) { // safe image in image cache self.imageCache[url.absoluteString] = image // Dispatch back to the main queue. DispatchQueue.main.async { completionHandler(image) } } }).resume() } }

Now we have the foundation for fetching and caching images. You were probably wondering where the mapping from the fetched products to the product image URLs happens. We will implement that now.

Go back to the loadData(:) method and add the following line of code directly below the product assignment. Your loadData(:) should look like this now:

private func loadData(completionHandler: @escaping () -> Void) {

    let supplierID = supplier.supplierID!
    let dataQuery = DataQuery().filter(Product.supplierID == supplierID).expand(Product.stockDetails)
    dataService?.fetchProducts(matching: dataQuery) { [weak self] products, error in
        // handle errors
        if let error = error {
            self?.logger.error("Error while fetching list of products for supplier: \(supplierID).", error: error)
            return
        }

        // set loaded products to property and reload data on the table view
        self?.products = products!

        // Use .map to create an array of picture URLs
        self?.productImageURLs = products!.map { $0.pictureUrl ?? "" }
        self?.tableView.reloadData()
        completionHandler()
    }
}

Like the last time we have to register a SAPFiori cell with the UITableView, but this time it is a FUIObjectTableViewCell. Add the following lines of code to the viewDidLoad() method right before the updateTableView(:) method call.

tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
tableView.separatorStyle = .none
tableView.backgroundColor = .preferredFioriColor(forStyle: .backgroundBase)

As the last step, we have to implement the table views data source methods similar to the SupplierTableViewController.

Add the following methods directly below the loadProductImageFrom(_:completionHandler:) method:

// MARK: - Table view data source

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return products.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let product = products[indexPath.row]

    let cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier) as! FUIObjectTableViewCell

    cell.headlineText = product.name ?? ""
    cell.subheadlineText = product.categoryName ?? ""
    cell.footnoteText = product.productID ?? ""
    cell.statusText = "Min Stock: \(product.stockDetails?.minStock?.doubleValue() ?? 0.0)"

    // set a placeholder image
    cell.detailImageView.image = FUIIconLibrary.system.imageLibrary

    // This URL is found in Mobile Services API tab
    let baseURL = "Your API found in the Sample Service assigned to your mobile app configuration in MS"
    let url = URL(string: baseURL.appending(productImageURLs[indexPath.row]))

    guard let unwrapped = url else {
        logger.info("URL for product image is nil. Returning cell without image.")
        return cell
    }

    // check if the image is already in the cache
    if let img = imageCache[unwrapped.absoluteString] {
        cell.detailImageView.image = img
    } else {
        // The image is not cached yet, so download it.
        loadProductImageFrom(unwrapped) { image in
            cell.detailImageView.image = image
        }
    }

    return cell
    }
}

Run the app and navigate to the SupplierProductsTableViewController.

Supplier Product List
Log on to answer question
Step 7: Implement the FUISearchBar to search for certain products in the list

Wouldn’t it be cool to also have a FUISearchBar which is inheriting from UISearchBar? - Of course, it would be, so let’s implement that.

NOTE: In case you’re not familiar with the UISearchBar or UISearchController read the official documentation:

Open the SupplierProductsTableViewController.swift class and add the following two class properties right below the products array:

private var searchController: FUISearchController?

// Will hold the searched for products in that array.
private var searchedProducts = [Product]()

Let’s implement a bit of setup logic for the FUISearchBar. Add the following method to your class:

private func setupSearchBar() {
    // Search Controller setup
    searchController = FUISearchController(searchResultsController: nil)

    // The SupplierProductsViewController will take care of updating the search results.
    searchController!.searchResultsUpdater = self
    searchController!.hidesNavigationBarDuringPresentation = true
    searchController!.searchBar.placeholderText = "Search for products"

    // No Barcode scanner needed here
    searchController!.searchBar.isBarcodeScannerEnabled = false

    self.tableView.tableHeaderView = searchController!.searchBar
}

Xcode will complain now because the SupplierProductsTableViewController.swift class is not conforming to the UISearchResultsUpdating protocol.

Add an extension to your class, like we did in the SupplierTableViewController:

extension SupplierProductsTableViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        // to implement
    }
}

If you run the app now you should see the FUISearchBar being displayed above the UITableView.

Supplier Product List

Now we have to implement some search logic to be called in the updateSearchResults(for:) method.

Implement the following methods right below the setupSearchBar() method and carefully read the inline comments.

// verify if the search text is empty or not
private func searchTextIsEmpty() -> Bool {
    return searchController?.searchBar.text?.isEmpty ?? true
}

// actual search logic for finding the correct products for the term the user is searching for
private func searchProducts(_ searchText: String) {
    searchedProducts = products.filter({( product : Product) -> Bool in
        return product.name?.lowercased().contains(searchText.lowercased()) ?? false
    })

    tableView.reloadData()
}

// verify if the user is currently searching or not
private func isSearching() -> Bool {
    return searchController?.isActive ?? false && !searchTextIsEmpty()
}

Cool! Let’s implement the updateSearchResults(for:) method:

extension SupplierProductsTableViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        // Get the searched-for term, note here that we don't have a time bouncer which waits for the user to finish its input. You could implement that if needed, for this simple example we do life searches for each character. I wouldn't recommend doing that over a large data set.
        if let searchText = searchController.searchBar.text {
            // Feed it to the search logic.
            searchProducts(searchText)
            return
        }
    }
}

As our last task here is to change the UITableView data source methods to use react to potential searches by the user.

Change the tableView(_:numberOfRowsInSection:) method to:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // if the user is searching display the searched for products and all products otherwise.
    return isSearching() ? searchedProducts.count : products.count
}

Also change the tableView(_:cellForRowAt:) method to retrieve the product either from the product array or in case of a search from the searchedProducts array.

Change the following line of code:

let product = products[indexPath.count]

to

let product = isSearching() ? searchedProducts[indexPath.row] : products[indexPath.row]

If you compile and run the app now you should see that you can search for products.

Searchbar Runtime
What protocol do you have to conform to, to handle search results? Check the correct answer.
×

Next Steps

Back to top