Skip to Content
Previous

Create a Fiori for iOS app in 50 minutes

By Robin van het Hof

Create a Fiori for iOS app in 50 minutes

Details

You will learn

In this tutorial, you will create a Fiori for iOS application which will show tracking info for purchased packages. This application has the following characteristics:

  • Connects to an SAP HANA MDC (Multi-tenant Database Container) XS OData service. It contains records of packages and their delivery status.
  • Use simplified OData querying with the SAP Cloud Platform SDK for iOS
  • Implement SAP Fiori for iOS controls to show timeline data
  • Displays deliveries turnaround times in a bar chart
  • Custom theming

When you are ready, your SAP Fiori for iOS application will resemble the following:

Final SAP Fiori for iOS application

Before you start, make sure you:


Step 1: Configure SAP Cloud Platform SDK for iOS Assistant

Note: If you have already configured the SAP Cloud Platform SDK for iOS Assistant, you can skip this step and proceed with “Step 2 - Run the SAP Cloud Platform SDK for iOS Assistant”.

This step provides simplified steps to configure the SAP Cloud Platform SDK for iOS Assistant application using the SAP Cloud Platform mobile service for development and operations cockpit.

Log on to your SAP Cloud Platform trial account at https://account.hanatrial.ondemand.com/ and once logged in, navigate to Services. Scroll down to Mobile Services and click on the Development & Operations tile. In the Development & Operations - Overview page, click the Go to Service link to open a new window to SAP Cloud Platform mobile service for development and operations.

Alternatively, you can go directly to https://hcpmsadmin-<your_user_id>trial.dispatcher.hanatrial.ondemand.com/.

SCPms landing page

Once you’re logged in to SAP Cloud Platform mobile service for development and operations, click the Important Links tab in the lower left bottom. The Important Links section opens.

Locate the tile SAP Cloud Platform SDK for iOS Assistant and click the Importing URLs directly into Assistant link:

Important Links

You should now see the following pop-up:

Import URLs

Click the Open SAP Cloud Platform SDK for iOS Assistant button. The SAP Cloud Platform SDK for iOS Assistant application will start. The New Account settings dialog will open, and both Admin API URL and Admin UI URL parameters are pre-populated automatically:

Import URLs

Provide the following additional details:

Field Value
Name A descriptive name for the configuration, for instance SAP Cloud Platform Mobile Services
Authentication Type Basic Authentication
User Your trial account user
Password Password for your trial account user
Import URLs

Click Add when finished. The account is now added to the SDK Assistant:

Import URLs

Close the Accounts dialog.

Please log in to access this content.
Step 2: Run the SAP Cloud Platform SDK for iOS Assistant

Note: If you went through “Step 1 - Configure SAP Cloud Platform SDK for iOS Assistant”, the SAP Cloud Platform SDK for iOS Assistant is already running and you may continue to “Step 3 - Create an Xcode Project”.

Double-click the SAP Cloud Platform SDK for iOS Assistant icon to start the application. If no applications have been generated previously, you will see the initial screen:

SDK Assistant
Please log in to access this content.
Step 3: Create an Xcode Project

Click the Plus button on the top-right of the SDK Assistant. The first page of the Xcode Project generation wizard lets you define the Project Properties.

Enter the following details:

Field Value
Product Name MyDeliveries
Author <your name>
Organization Name <your company name>
Organization Identifier com.sap.tutorials.demoapp
Destination <choose a local destination>
Project Properties

Click Next to advance to the SAP Cloud Platform mobile service for development and operations Configuration step.

Please log in to access this content.
Step 4: SAP Cloud Platform mobile service for development and operations Configuration details

In the SAP Cloud Platform mobile service for development and operations Configuration page, select the Create tab button.

Enter the following details:

Field Value
Application Name MyDeliveries
Application Identifier com.sap.tutorials.demoapp.MyDeliveries
Authentication Type OAuth
Use Existing

Click Next to advance to the OData Services step.

Please log in to access this content.
Step 5: OData Services

In the OData Services page, you can define the back end connection. Here you will add the OData endpoint for the DeliveryService OData service.

OData Services

Click the Plus button, and from the context menu, select New Destination…. A dialog opens:

OData Services

At the General tab, enter the following details:

Field Value
Backend URL https://sapdevsdd27584c4.us2.hana.ondemand.com/codejam/wwdc/services/DeliveryService.xsodata

Expand the Advanced destination options node, and set the following:

Field Value
Proxy Type Internet
URL rewrite mode Rewrite URL
Maximum connections Server default
OData Services

At the Authentication tab, make sure Authentication Type is set to No Authentication.

OData Services

Click OK to save the backend configuration. It is now listed in the available destinations:

OData Services

Click Next to advance to the Optional Features step.

Please log in to access this content.
Step 6: Optional Features

In the Optional Features page, you have the option to generate a Master-Detail Application, enable logging and log uploads, and enable remote notifications.

Optional Features

Make sure the checkboxes Generate Master-Detail Application, Enable Logging and Enable Log Upload are selected and click Finish to complete the wizard.

Most likely the checkbox for Remote Notifications is disabled. This happens because no APNS endpoint is configured for the application definition in SAP Cloud Platform mobile service for development and operations. Once configured with a valid certificate, this option becomes available.

Please log in to access this content.
Step 7: Generating the Xcode project

After you have clicked Finish in the previous step, the SDK Assistant now loads the OData service’s metadata. This metadata describes the data model, and can be accessed via <service URL>$metadata. For your service, the metadata URL would be https://sapdevsdd27584c4.us2.hana.ondemand.com/codejam/wwdc/services/DeliveryService.xsodata/$metadata
Based on this metadata, the OData proxy classes will be generated for the Xcode project.

In addition, the configuration settings you have provided in the SDK Assistant are now being sent to SAP Cloud Platform mobile service for development and operations.

NB: If you have already 5 native applications defined in SAP Cloud Platform mobile service for development and operations, the SDK Assistant will give the following error:

Optional Features

In that case, log on to your SAP Cloud Platform mobile service for development and operations account at https://hcpmsadmin-<your_user_id>trial.dispatcher.hanatrial.ondemand.com/ and navigate to Mobile Applications > Native/Hybrid. Select one of the available application configurations and delete in order for the SDK Assistant to add the new application configuration.

Please log in to access this content.
Step 8: Examine the generated Xcode Project

After the SDK Assistant has finished, Xcode will launch and open the just generated MyDeliveries project.

Xcode project overview

The Main.storyboard shows split-view setup for the generated Master-Detail views.

Folder MyDeliveries/Onboarding contains logic for the user on-boarding, authentication and handling of pass-codes and Touch ID.

Folder Proxy Classes contains the OData proxy classes generated from the OData service. File DeliveryService.swift acts as a data service provider to gain access to the OData entities. The two files PackagesType.swift and DeliveryStatusType.swift are classes for the OData entities Packages and DeliveryStatus, respectively. These classes give access to the various properties of the OData entities.

Folders ViewControllers/Packages and ViewControllers/DeliveryStatus contain the master and detail view controllers as well as a storyboard for the Packages and DeliveryStatus entities, respectively.

Please log in to access this content.
Step 9: Build and run the generated application

Click the Run button to build and run the generated application:

Build and run

The Simulator app now launches. If you have configured the app to allow for push notifications, you will get the following pop-up:

Build and run

Press Allow. You now see the initial landing page:

Build and run

The application name is shown, with a little description. You have the option to show a demo version of the application (this should be implemented by hand, as this is not generated by the iOS Assistant) or run the actual, live application.

In this tutorial, you use the live application. Clicking the blue Start button to proceed.

The OAuth login screen of SAP Cloud Platform mobile service for development and operations is shown. Enter your login credentials for the SAP Cloud Platform and press the Log On button:

Build and run

The app now displays a sample EULA. Click Agree to proceed.

Build and run

The app starts with an overview of the available Collections of the OData service:

Build and run
Please log in to access this content.
Step 10: Examine the generated application

If you click on the Packages collection, you navigate to a Master list with all available Package entities:

Master screen

If you click on one of the Package entities, you navigate to a Detail page which lists all the properties for the selected entity:

Detail screen
Please log in to access this content.
Step 11: Introduction to the SDK's OData API

The generated application demonstrates the OData proxy classes are working, browse their properties, and demonstrates push notifications and the various authentication mechanisms. For productive use, it is recommended to start with a new, empty project, and use parts of the generated app into your own project.

However, to show how to access backend data via OData in an object-oriented way, we will extend the generated application.

Examine the OData service’s metadata, which can be accessed via https://sapdevsdd27584c4.us2.hana.ondemand.com/codejam/wwdc/services/DeliveryService.xsodata/$metadata:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
  <edmx:DataServices m:DataServiceVersion="2.0" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <Schema Namespace="codejam.wwdc.services.DeliveryService" xmlns="http://schemas.microsoft.com/ado/2008/09/edm" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
      <EntityType Name="DeliveryStatusType">
        <Key>
          <PropertyRef Name="DeliveryStatusID"/>
        </Key>
        <Property MaxLength="36" Name="DeliveryStatusID" Nullable="false" Type="Edm.String"/>
        <Property MaxLength="36" Name="PackageID" Type="Edm.String"/>
        <Property MaxLength="256" Name="Location" Type="Edm.String"/>
        <Property Name="DeliveryTimestamp" Type="Edm.DateTime"/>
        <Property MaxLength="16" Name="StatusType" Type="Edm.String"/>
        <Property Name="Selectable" Type="Edm.Int32"/>
        <Property MaxLength="128" Name="Status" Type="Edm.String"/>
      </EntityType>
      <EntityType Name="PackagesType">
        <Key>
          <PropertyRef Name="PackageID"/>
        </Key>
        <Property MaxLength="36" Name="PackageID" Nullable="false" Type="Edm.String"/>
        <Property MaxLength="256" Name="Name" Type="Edm.String"/>
        <Property MaxLength="256" Name="Description" Type="Edm.String"/>
        <Property Name="Price" Precision="10" Scale="2" Type="Edm.Decimal"/>
        <NavigationProperty FromRole="PackagesPrincipal" Name="DeliveryStatus" Relationship="codejam.wwdc.services.DeliveryService.PackageDeliveryStatusType" ToRole="DeliveryStatusDependent"/>
      </EntityType>
      <Association Name="PackageDeliveryStatusType">
        <End Multiplicity="1" Role="PackagesPrincipal" Type="codejam.wwdc.services.DeliveryService.PackagesType"/>
        <End Multiplicity="*" Role="DeliveryStatusDependent" Type="codejam.wwdc.services.DeliveryService.DeliveryStatusType"/>
        <ReferentialConstraint>
          <Principal Role="PackagesPrincipal">
            <PropertyRef Name="PackageID"/>
          </Principal>
          <Dependent Role="DeliveryStatusDependent">
            <PropertyRef Name="PackageID"/>
          </Dependent>
        </ReferentialConstraint>
      </Association>
      <EntityContainer Name="DeliveryService" m:IsDefaultEntityContainer="true">
        <EntitySet EntityType="codejam.wwdc.services.DeliveryService.DeliveryStatusType" Name="DeliveryStatus"/>
        <EntitySet EntityType="codejam.wwdc.services.DeliveryService.PackagesType" Name="Packages"/>
        <AssociationSet Association="codejam.wwdc.services.DeliveryService.PackageDeliveryStatusType" Name="PackageDeliveryStatus">
          <End EntitySet="Packages" Role="PackagesPrincipal"/>
          <End EntitySet="DeliveryStatus" Role="DeliveryStatusDependent"/>
        </AssociationSet>
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>

As you can see, it is a fairly simple data model containing two Entity Sets (or tables) called Packages and DeliveryStatus. Each entity in the set (or record) is identified as a PackagesType and DeliveryStatusType, respectively.

There is also an association between PackagesType and DeliveryStatusType, where a single PackagesType can have related DeliveryStatusType’s with a 0..n cardinality.

We will now show for each PackageType its related DeliveryStatusType’s, shown in a timeline in ascending order (newest on top). The timeline will be build using SAP Fiori for iOS controls.

Using the SDK’s SAPOData framework, you can create OData queries in a really simple way. Instead of executing SQL statements, the SDK provides a ‘fluent interface’ or ‘method chaining’ approach to constructing queries, which makes the code much more readable.

A query to get all DeliveryStatus entities for a particular Package would then be something like this:

 // Method 1
 let query = DataQuery()
     // SELECT * FROM DeliveryStatus
     .from(DeliveryServiceMetadata.EntitySets.deliveryStatus)
     // WHERE DeliveryStatus.packageID == <selected package ID>
     .where(DeliveryStatusType.packageID.equal((currentEntity?.packageID)!))

The result of this query is an array of DeliveryStatusType objects.

With OData, you can even have greater flexibility. Since there is a one-to-many association (or ‘Navigation Link’) between Package and DeliveryStatus, you could also load the Package object and all related child DeliveryStatus entities at once:

 // Method 2
 let query = DataQuery()
     // SELECT * FROM Packages
     .from(DeliveryServiceMetadata.EntitySets.packages)
     // WHERE <primary key> = <selected package ID>
     .withKey(PackagesType.key(packageID: currentEntity?.packageID))
     // LEFT JOIN DeliveryStatus ON <abstracted, defined in association>
     .expand(PackagesType.deliveryStatus)

Using the generated OData Proxy classes, you can then simply access the PackagesType related DeliveryStatusType objects:

Proxy class

NB: Since the SDK Assistant generated app by default does not support OData Navigation Links, it takes a bit more effort to enable this. Furthermore, sorting an expanded entity set is only supported in OData V4, and this tutorial uses an OData V2 service. Therefore, in this tutorial we’ll simply query the DeliveryStatus entities for each Package.

Please log in to access this content.
Step 12: Create a new Table View Controller

To list the tracking info for each package using SAP Fiori Timeline controls, the most simple approach would be to create a new Table View Controller and implement the code needed to display the statuses.

In Xcode, locate the file MyDeliveries/ViewControllers/Packages/Packages.storyboard and open the file:

New Table View Controller

Drag a Table View Controller object from the Object library onto the Storyboard, next to the Detail Scene.

With the just added Table View Controller selected, give it the name Tracking Info in the Attribute inspector:

New Table View Controller
Please log in to access this content.
Step 13: Create new subclass of a UITableViewController class

Right-click the MyDeliveries/ViewControllers/Packages folder in your project, and from the context menu choose New File….

In the dialog, select Cocoa Touch Class:

New Table View Controller subclass

Click Next.

Provide the following details:

Field Value
Class TrackingInfoViewController
Subclass Of UITableViewController
New Table View Controller subclass

Click Next. In the next screen, make sure the new class file is stored in group Packages and click Create.

Switch to the Packages.storyboard file and select the Tracking Info Table View. In the Identity inspector, set the Custom Class to TrackingInfoViewController:

Link Table View Controller to subclass

To avoid a “prototype table cells must have reuse identifiers” warning, you can provide an identifier for the table view’s prototype cell, or alternatively, set the Prototype Cells value to 0.

Please log in to access this content.
Step 14: Add navigation Table View Cell to Detail Table View

Drag a Table View Cell onto the Detail Table View, and set the following properties in the Attribute inspector:

Field Value
Identifier NavToShowTrackingInfo
Accessory Disclosure Indicator
Create Table View Cell

Control-click the just added Table View Cell and drag it onto the Tracking Info Scene. From the Segue pop-up, choose Show.

With the segue selected, go to the Attributes inspector and provide the name showTrackingInfo as its Identifier:

Create Segue
Please log in to access this content.
Step 15: Add initializer code to TrackingInfoViewController

In the newly created file TrackingInfoViewController.swift, replace the import UIKit statement with the following import statements:

import SAPFoundation
import SAPCommon
import SAPOData
import SAPFiori

Just below the line class TrackingInfoViewController: UITableViewController {, add the following declarations:

private let appDelegate = UIApplication.shared.delegate as! AppDelegate
private let logger: Logger = Logger.shared(named: "TrackingInfoViewController")

private var _entities: [DeliveryStatusType] = [DeliveryStatusType]( )
var entities: [EntityValue] {
    get { return _entities }
    set { self._entities = newValue as! [DeliveryStatusType]
    }
}

This adds a reference to the AppDelegate class, a reference to the SDK’s logging mechanism, and fields to set/get the selected DeliveryStatusType entity.

Please log in to access this content.
Step 16: Implement Table View Cell for Packages

Open file ./MyDeliveries/ViewControllers/Packages/PackagesDetailViewController.swift.

Locate method tableView(_: UITableView, numberOfRowsInSection _: Int). Currently it returns 4 rows, the total number of properties the Package entity has. However, since you added an extra Table View Cell to navigate to the Tracking Info scene, you want to make this extra cell visible.

Set the return value to 5:

override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
    return 5
}

Next, locate method tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath).

It contains a switch statement which, depending on the indexPath.row value, displays the property and corresponding value for the selected Package.

To display the added Table View Cell, add an extra case statement, just above the default: switch:

case 4:
    let navigationLink = tableView.dequeueReusableCell(withIdentifier: "NavToShowTrackingInfo",
        for: indexPath) as UITableViewCell
    navigationLink.textLabel?.text = "Show Tracking Info..."
    return navigationLink

Now the 5th row in the table will return a Table View Cell matching identifier NavToShowTrackingInfo, and it will display the static text Show Tracking info....

If you now run and build the application, the newly added table cell is displayed:

New navigation cell displayed

However, if you click on it, nothing happens… You will solve that in the next step.

Please log in to access this content.
Step 17: Implement navigation logic

In the previous step, you have noticed the navigation to the Tracking Info scene did not happen. That is caused since the Table View in the Detail Scene inside the Packages.storyboard file has turned off its ability to select table cells.

Open the file ./MyDeliveries/ViewControllers/Packages/Packages.storyboard and from the Detail Scene, select its Table View.

From the Attributes inspector, locate the Selection attribute and set its value to Single Selection:

New navigation cell displayed

Next, open the file ./MyDeliveries/ViewControllers/Packages/PackagesDetailViewController.swift.

Scroll down a bit and locate the segue method prepare(). Below the existing if statement, add the following code:

if segue.identifier == "showTrackingInfo" {
    if (self.tableView.indexPathForSelectedRow?.row != nil) {
        let trackingInfoView = segue.destination as! TrackingInfoViewController

        let currentEntity = self.entity as PackagesType

        let esDeliveryStatus = DeliveryServiceMetadata.EntitySets.deliveryStatus
        let propPackageId    = DeliveryStatusType.packageID
        let propTimestamp    = DeliveryStatusType.deliveryTimestamp

        // Load all related DeliveryStatuses for the current Package,
        // latest first.
        let query = DataQuery()
            .from(esDeliveryStatus)
            .where(propPackageId.equal((currentEntity.packageID)!))
            .orderBy(propTimestamp, SortOrder.descending)

        self.deliveryService.fetchDeliveryStatus(matching: query) { deliveryStatus, error in
            guard let deliveryStatus = deliveryStatus else {
                return
            }
            trackingInfoView.entities = deliveryStatus
            trackingInfoView.tableView.reloadData()
        }
    }
}

With this code, you create a query to load all DeliveryStatus entities for the selected Package entity, and store the results into the TrackingInfoViewController.

Please log in to access this content.
Step 18: Explore SAP Fiori Timeline cells with the SAP Fiori Mentor app

You now have enabled navigation, as well as created a query to load all related DeliveryStatus entities. However, you haven’t bound the results to table cells yet.

Since we want to display the DeliveryStatus items in a timeline, the best way to achieve this is to use the SDK’s FUITimeline table view cell control. A great tool for exploring SAP Fiori for iOS controls and help implementing these into your project is the SAP Fiori Mentor app. This is a companion tool to the SDK, and can be downloaded for iPad from the Apple App Store.

Open the SAP Fiori Mentor app on your iPad. Upon opening, the app shows an overview page:

Mentor app

Click on the See All link next to the UI Components section, and scroll down until you see the Timeline Cell tile:

Mentor app

Click the Timeline Cell tile. You now see a page wit a representation of the SAP Fiori Timeline cell, and a couple of preset styles to change the look and feel for the control.

Mentor app

You can also customize the look and feel on a more granular level. Click the button with three dots in the lower right corner. This will bring a pop up where you can specify different settings for the control. The control’s look and feel is instantly updated, giving you an idea of the final result:

Mentor app

When you’re happy with the final result, click the Code button (the one labeled </>). This will bring a pop up with a sample UITableViewController class, and all the properties you have set or enabled in the Control Settings pop-up are reflected in the generated code:

Mentor app

To use the generated code in Xcode, click the Share button in the top-right, and use AirDrop to transfer to your Mac:

Mentor app

Open the downloaded text file:

Mentor app

The generated code can now be implemented into the appropriate places in the TrackingInfoViewController.swift file.

NOTE Since it may take a bit too long to go through the steps of copying and pasting the code, adding the control binding to the Proxy Classes’ properties and format the data properly, you don’t need to do this yourself. The code to implement will be provided in the next tutorial.

Please log in to access this content.
Step 19: Initialize table layout

In this step, you implement Fiori Timeline cells to show the DeliveryStatus entities in a logical way.

Open the file ./MyDeliveries/ViewControllers/Packages/TrackingInfoViewController.swift and locate the method viewDidLoad().

Replace the commented-out part with the following code:

tableView.register(FUITimelineCell.self, forCellReuseIdentifier: "FUITimelineCell")
tableView.register(FUITimelineMarkerCell.self, forCellReuseIdentifier: "FUITimelineMarkerCell")
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableViewAutomaticDimension
tableView.backgroundColor = UIColor.preferredFioriColor(forStyle: .backgroundBase)
tableView.separatorStyle = .none

NOTE The above code originated from the SAP Fiori for iOS Mentor app, but has been slightly modified to show both FUITimelineCell and FUITimelineMarkerCell control.

Please log in to access this content.
Step 20: Implement table row methods

Next, locate method numberOfSections(in tableView:).

Change it so it returns 1 section:

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

Now, locate method tableView(_ tableView:, numberOfRowsInSection section:).

Change it to return the number of loaded entities:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self._entities.count
}
Please log in to access this content.
Step 21: Implement FUITimelineCell logic

Finally, remove the remaining commented-out methods and replace them with these methods:

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

    let deliverystatustype = self._entities[indexPath.row]

    if deliverystatustype.selectable != 0 {
        return self.getFUITimelineCell(deliverystatustype: deliverystatustype, indexPath: indexPath)
    }
    else {
        return self.getFUITimelineMarkerCell(deliverystatustype: deliverystatustype, indexPath: indexPath)
    }
}

private func getFUITimelineMarkerCell(deliverystatustype: DeliveryStatusType, indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "FUITimelineMarkerCell", for: indexPath)
    guard let timelineCell = cell as? FUITimelineMarkerCell else {
        return cell
    }
    timelineCell.nodeImage = self.getNodeImage(statusType: deliverystatustype.statusType!)
    timelineCell.showLeadingTimeline = indexPath.row == 0 ? false : true
    timelineCell.showTrailingTimeline = indexPath.row == self._entities.count - 1 ? false : true
    timelineCell.eventText = self.getFormattedDateTime(timestamp: deliverystatustype.deliveryTimestamp!)
    timelineCell.titleText = deliverystatustype.status

    return timelineCell
}

private func getFUITimelineCell(deliverystatustype: DeliveryStatusType, indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "FUITimelineCell", for: indexPath)
    guard let timelineCell = cell as? FUITimelineCell else {
        return cell
    }
    timelineCell.nodeImage = self.getNodeImage(statusType: deliverystatustype.statusType!)
    timelineCell.eventText = self.getFormattedDateTime(timestamp: deliverystatustype.deliveryTimestamp!)
    timelineCell.headlineText = deliverystatustype.status
    timelineCell.subheadlineText = deliverystatustype.location

    return timelineCell
}

private func getFormattedDateTime(timestamp: LocalDateTime) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "MM/dd HH:mm"

    return formatter.string(from: timestamp.utc())
}

private func getNodeImage(statusType: String) -> UIImage {
    switch statusType {
    case "start"    : return FUITimelineNode.start
    case "inactive" : return FUITimelineNode.inactive
    case "complete" : return FUITimelineNode.complete
    case "earlyEnd" : return FUITimelineNode.earlyEnd
    case "end"      : return FUITimelineNode.end
    default         : return FUITimelineNode.open
    }
}

The first method tableView(_ tableView:, cellForRowAt indexPath:) decides based on DeliveryStatus property selectable which specific timeline cell to render. This rendering is done via two private methods getFUITimelineMarkerCell(deliverystatustype:, indexPath:) and getFUITimelineCell(deliverystatustype:, indexPath:).

These two private methods are implemented based on the code from the SAP Fiori for iOS Mentor app, but the code from the Mentor app has been split into two separate functions and control binding has already been implemented for easier implementation in this tutorial.

.

The final two private methods are helpers to format the timestamp into something more readable, and to get the correct FUITimelineNode image indicator based on the DeliveryStatus property StatusType.

Please log in to access this content.
Step 22: Run the application

Build and run the application. Navigate to the Packages master page and select a package. If you now click on the Show Tracking Info… cell, you’ll navigate to the Tracking Info scene, and the Package’s related DeliveryStatus records are now shown in descending order using two flavors of the Fiori Timeline cell control.

Timeline
Please log in to access this content.
Step 23: Data Visualization example

In this tutorial step, you will be introduced to the data visualization capabilities of the SDK. For brevity, some static data is used, but this can easily be changed to OData entities, like you did with the implementation of the Timeline Cell in the previous steps.

In the Project navigator, navigate to the MyDeliveries/ViewControllers/Packages folder. Right-click this folder, and from the context menu, select New File…

In the dialog, select Cocoa Touch Class:

New View Controller subclass

Click Next.

Provide the following details:

Field Value
Class ChartViewController
Subclass Of UIViewController
New View Controller subclass

Click Next to continue. Check that the file is saved in the Packages group, and click Create to finalize the wizard. The new file will now open.

However, the Cocoa Touch class you have just created subclasses UIViewController. In order to show the SDK’s data visualizations, it should subclass FUIChartFloorplanViewController.

First, add the necessary import statements:

import SAPFoundation
import SAPFiori
import SAPCommon

Then, change the signature of the class so it now extends from FUIChartFloorplanViewController:

class ChartViewController: FUIChartFloorplanViewController {

Now you have the scaffolding for the data visualizations class. We’ll leave it for now, the actual implementation will be finalized in a later step.

Step 24: Add View Controller to Storyboard

Open the Packages.storyboard file, and from the Object library, drag a View Controller onto the storyboard. Set the title to Chart View:

Create View Controller

Now switch to the Identity inspector and set the Custom Class to ChartViewController:

Create View Controller

Drag a Table View Cell onto the Detail Table View, and set the following properties in the Attribute inspector:

Field Value
Identifier NavToShowChart
Accessory Disclosure Indicator
Create Table View Cell

Control-click the just added Table View Cell and drag it onto the Chart View Scene. From the Segue pop-up, choose Show.

With the segue selected, go to the Attributes inspector and provide the name showChart as its Identifier:

Create Segue
Step 25: Implement Table View Cell for Chart

Open file ./MyDeliveries/ViewControllers/Packages/PackagesDetailViewController.swift.

Locate method tableView(_: UITableView, numberOfRowsInSection _: Int). Currently it returns 5 rows, the total number of properties the Package entity has. However, since you added an extra Table View Cell to navigate to the Chart View scene, you want to make this extra cell visible.

Set the return value to 6:

override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
    return 6
}

Next, locate method tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath).

To display the added Table View Cell, add an extra case statement, just above the default: switch:

case 5:
    let navigationLink = tableView.dequeueReusableCell(withIdentifier: "NavToShowChart",
        for: indexPath) as UITableViewCell
    navigationLink.textLabel?.text = "Show Waiting Time..."
    return navigationLink
Please log in to access this content.
Step 26: Implement Chart View Controller

In the Project navigator, navigate to the MyDeliveries/ViewControllers/Packages folder and open the ChartViewController.swift file you have created in step 23.

Replace the viewDidLoad method with the following:

override func viewDidLoad() {
    super.viewDidLoad()

    title = "Waiting Time"
    chartView.chartType = .bar
    chartView.numberOfGridlines = 4
    chartView.dataSource = self

    summaryView.dataSource = self
    titleText.text = "Duration"
    status.text = "Click chart for details"
    categoryAxisTitle.text = "Location"
    valuesAxisTitle.text = "Waiting time in hours"
}

This sets the default settings for the chart, in this case, a bar chart.

A couple of errors are now shown. That is because the chart’s data source is not yet implemented.

At the bottom of the file, add the following two extensions:

extension ChartViewController: FUIChartSummaryDataSource {

    func chartView(_ chartView: FUIChartView, summaryItemForCategory categoryIndex: Int) -> FUIChartSummaryItem? {

        let item = FUIChartSummaryItem()
        item.categoryIndex = categoryIndex
        item.isPreservingTrendHeight = false

        switch categoryIndex {
        case -1:
            item.isEnabled = false

            let values: [Double] = {
                var values: [Double] = []
                for series in chartView.series {
                    let categoriesUpperBound = series.numberOfValues - 1
                    if let valuesInSeries = series.valuesInCategoryRange((0...categoriesUpperBound), dimension: 0) {
                        values.append(valuesInSeries.compactMap({ $0 }).reduce(0.0, +))
                    }
                }
                return values
            }()

            let numberFormatter  = NumberFormatter()
            numberFormatter.maximumFractionDigits = 0

            item.valuesText = values.map { "\(numberFormatter.string(from: $0 as NSNumber)!) hours" }
            item.title.text = "Total wait time"

        default:
            item.isEnabled = true

            let values: [Double] = {
                var values: [Double] = []
                for series in chartView.series {
                    values.append(series.valueForCategory(categoryIndex, dimension: 0)!)
                }
                return values
            }()

            item.valuesText = values.map { formattedTitleForDouble($0)! }
            item.title.text = chartCategoryTitles()[categoryIndex]
        }

        return item
    }
}

extension ChartViewController: FUIChartViewDataSource {

    // MARK: - FUIChartViewDataSource methods
    func numberOfSeries(in: FUIChartView) -> Int {
        return chartData().count
    }

    func chartView(_ chartView: FUIChartView, numberOfValuesInSeries seriesIndex: Int) -> Int {
        return chartData()[seriesIndex].count
    }

    func chartView(_ chartView: FUIChartView, valueForSeries series: Int, category categoryIndex: Int, dimension dimensionIndex: Int) -> Double? {
        return chartData()[series][categoryIndex]
    }

    func chartView(_ chartView: FUIChartView, formattedStringForValue value: Double, axis: FUIChartAxisId) -> String? {
        return formattedTitleForDouble(value)
    }

    func chartView(_ chartView: FUIChartView, titleForCategory categoryIndex: Int, inSeries seriesIndex: Int) -> String? {
        return chartCategoryTitles()[categoryIndex]
    }

    // MARK: - helper methods for generating & formatting sample dat

    func chartSeriesTitles() -> [String] {
        return ["Actual", "Target"]
    }
    func chartCategoryTitles() -> [String] {
        return ["Shipment picked up", "HONG-KONG", "AMSTERDAM", "LONDON-HEATHROW", "READING", "Delivered"]
    }

    func chartData() -> [[Double]] {
        return [[2, 42, 32, 7, 5, 1]]
    }

    func formattedTitleForDouble(_ value: Double) -> String? {
        let numberFormatter = NumberFormatter()
        numberFormatter.maximumFractionDigits = 0
        return numberFormatter.string(from: value as NSNumber)
    }

}

The first extension draws the chart items.

The chart item at categoryIndex value -1 is the “pinned” or “fixed position” item in the chart’s summary header.
The chart item at the other or default positions are the actual chart items.

The second extension supplies the data to the chart. Here you see the hard-coded values for the category titles and chart data.

If you now build and run the application, and click on one of the Packages entities, you now see the added link to the chart:

Chart View

If you click the Show Waiting Time… link, you now see the bar chart with the delivery waiting times, and calculated total waiting time (89 hours):

Chart View

If you now click on one of the bars in the chart, the item’s details are shown in the summary header:

Chart View
Please log in to access this content.
Step 27: Create a NUI stylesheet

In these final tutorial steps, you will apply a custom theme to your iOS app using NUI. NUI enables you to style iOS components with style sheets similar to CSS. NUI is already integrated in the SAP Cloud Platform SDK for iOS so you don’t need to install anything.

In Xcode, right-click the MyDeliveries folder and from the context menu, select New File…. In the dialog, scroll down to the Other section and select the Empty template:

Create a NUI stylesheet

Click Next to proceed.

In the next screen, provide the following details:

Field Value
File Name CustomTheme.nss
Create a NUI stylesheet

Make sure it is saved in the MyDeliveries group and click Create. The new CustomTheme.nss file is now created in the root of your project.

Please log in to access this content.
Step 28: Add styles to the stylesheet

The styles in the stylesheet can be applied to both standard iOS components such as UINavigationBar, UITableView etc. as well as SAP Fiori for iOS components.

For a reference of the standard iOS components classes, you can refer to NUI style classes.

For SAP Fiori for iOS components style classes, the following conventions should be followed:

  • Global definitions
  • fdl<lower camelcase enum name>_<property name>
  • example: fdlFontStyle_subheadline
  • SAP Fiori component specific definitions

  • fdl<class name>_<property name>
  • example: fdlFUIWelcomeScreen_primaryActionButton

Open the just created CustomTheme.nss file, and add the following:

NavigationBar {
    bar-tint-color: #B0D450;
}

BarButton {
    font-color: #3A835B;
}

/* Onboarding Welcome Screen */
fdlFUIWelcomeScreen_headlineLabel {
    font-color: #3A835B;
}

fdlFUIWelcomeScreen_primaryActionButton {
    background-color-normal: #3A835B;
    background-color-highlighted: #B0D450;
}

/* Fiori subheadline */
fdlFontStyle_subheadline {
    font-style: subheadline;
    font-color: #3A835B;
}

/* Fiori Timeline cells */
fdlFUITimelineCell, fdlFUITimelineMarkerCell {
    background-color: #E0F0B9;
}

fdlFUITimelineCell_timelineBackground,
fdlFUITimelineMarkerCell_cardBackground,
fdlFUITimelineMarkerCell_timelineBackground {
    background-color: #E0F0B9;
}

/* Fiori Data Vizualization */
fdlFUIChartFloorplanViewController_title,
fdlFUIChartFloorplanViewController_seriesTitles,
fdlFUIChartFloorplanViewController_valuesAxisTitle,
fdlFUIChartFloorplanViewController_categoryAxisTitle {
    font-color: #3A835B;
}

This adds a light-green tint to the standard iOS navigation bar as well as a darker green for the navigation bar buttons.

The standard SAP Fiori subheadline font style (member of the SDK’s SAPFiori FDLFontStyle enum) is also changed to green, as is the on-boarding’s application title and primary action button.

The SAP Fiori Timeline cells get a light green background, and the SAP Fiori Data Visualization chart texts will be the same dark green as the headlines.

The chart bars are normally not styled with a stylesheet, since you would rather have them colored based on their context and/or value.

However, you could easily change the default Fiori blue to a dark green color by adding the following line to the chartView method inside the ChartViewController: FUIChartSummaryDataSource extension:

swift chartView.series.colors = [UIColor(hexString: "#3A835B")]

Please log in to access this content.
Step 29: Load stylesheet

In order for your app to apply the custom styles, you need to tell your app to use the custom stylesheet.

Open the app’s AppDelegate.swift file, and in method application(_:didFinishLaunchingWithOptions:), at the top of the method’s body, add the following line:

NUISettings.initWithStylesheet(name: "CustomTheme")

This tells your app to use NUI with your custom stylesheet CustomTheme.nss.

Please log in to access this content.
Step 30: Build and run the app

First, remove the app from your device, so you will go through the onboarding again.

Then build and run the app.

When launched, you should now see the restyled on-boarding screen with the greenish theme:

Create a NUI stylesheet

If you proceed further, you will see the navigation bar is also styled:

Create a NUI stylesheet

And, unsurprisingly, the custom UI you have created earlier follows the same theme:

Create a NUI stylesheetCreate a NUI stylesheet

For more on theming SAP Fiori for iOS components, see Branding & Theming

For more on NUI, see NUI readme

Please log in to access this content.

Updated 05/31/2018

Time to Complete

50 Min

Intermediate

Prerequisites

Next
Back to top