Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Dan_vL
Product and Topic Expert
Product and Topic Expert
Previous (Offline OData)   Home

SAP Fiori for iOS includes a set of additional controls that inherit from controls in Apple UIKit. The SAP Fiori Mentor App for iPads can be used to explore and learn more about these controls.

The following are some additional sources of documentation on SAP Fiori for iOS.
Fiori for iOS Design Guidelines
SAPFiori API Docs
Branding and Theming
List Report Floorplan Tutorial
Timeline Tutorial

The following steps will provide some simple examples of using the SAP Fiori for iOS controls.

Adding a Toast and Showing a Loading Indicator
Adding a FUIFilterFeedbackControl
Using and Enhancing a Map

Adding a Toast and Loading Indicator


The following steps will display the results of the OData query using a toast and will show the loading activity indicator while the offline store is being opened which can take a few seconds the first time it is opened.

  1. The SAP Fiori Mentor App can be downloaded from the App Store onto an iPad.

  2. Under the UI Components section of the app, select the Toast Message component.

  3. The control's settings can be modified as shown below.

  4. Sample code is shown by pressing the following button.


  5. Add the following import to the top of ViewController.swift.
    import SAPFiori

    Add the following code to make use of a FUIToastMessage.
    In the getProductsOffline method, comment out the call to the print method which writes out the result with the following code.
    let msg = "Offline: got \(products.count) products and the first product name is \(products[0].name!)"
    print(msg)
    FUIToastMessage.show(message: msg, icon: UIImage(), inWindow: self.view.window, withDuration: 3.0, maxNumberOfLines: 3)


  6. Run the app and press the button to call getProductsOffline. Notice the result is now shown in a toast.

  7. The following changes will make use of a FUILoadingIndicatorView to indicate that the app is downloading the offline store.
    Add the following line of code to the top of the method getProductsOffline.
    let loadingView = FUIModalLoadingIndicator.show(inView: self.view, animated: true)

    Inside of the oDataProvider.open method, add the following code.
    defer {
    DispatchQueue.main.async {
    loadingView.dismiss()
    }
    }


  8. Delete the app from the simulator, re-run, press the button which triggers the download of an offline database from the server and notice there is now a loading indicator.


Adding a FUIFilterFeedbackControl


The following example demonstrates how to add a table with a filter control.

  1. Under the UI Components section of the SAP Fiori Mentor app, select the Filter Feedback component.

  2. Notice that the filter is interactive. In the screen shot below, a sort order by Price has been specified as well as a filter to show only Computer Systems.

  3. The source code to try out the control is available by pressing the following button.


  4. Create a new project named FilterSample.

  5. Add the embedded binaries for the SAP Cloud Platform SDK for iOS.

  6. Add a new Table View Controller and drag the storyboard entry point to point at the newly added screen.

  7. Create a new file of type Cocoa Touch Class named FilterFeedbackTableViewController that extends UITableViewController and replace its contents with those taken from the SAP Fiori Mentor App.

    The source code is included below in case you do not have an iPad which is required for the SAP Fiori Mentor app.
    import SAPFiori
    import UIKit

    struct Product {
    var image: UIImage
    var name: String
    var id: String
    var mainCategoryName: String
    var description: String
    var price: Int
    }

    class FilterFeedbackExample: UITableViewController {

    let currency = "USD"

    var products = [Product]()
    var productsToDisplay = [Product]()
    let estimatedRowHeight = CGFloat(80)

    var filterFeedbackControl: FUIFilterFeedbackControl!
    var categoryGroup = FUIFilterGroup()

    let categoryItemComputerSystems = FUIFilterItem("Computer Systems", isFavorite: true, isActive: false)
    let categoryItemSmartphonesTablets = FUIFilterItem("Smartphones & Tablets", isFavorite: true, isActive: false)
    let categoryItemPrintersScanners = FUIFilterItem("Printers & Scanners", isFavorite: true, isActive: false)

    override func viewDidLoad() {
    super.viewDidLoad()

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

    tableView.estimatedRowHeight = estimatedRowHeight
    tableView.rowHeight = UITableView.automaticDimension

    tableView.separatorStyle = .singleLine

    categoryItemComputerSystems.key = "CS"
    categoryItemSmartphonesTablets.key = "ST"
    categoryItemPrintersScanners.key = "PS"

    productsToDisplay = getProducts()
    products = getProducts()
    setupCategoryGroups()
    setupFilterFeedback()
    }

    func getProducts() -> [Product] {

    return [

    Product(image: UIImage(), name: "Photo Scan", id: "HT-1080", mainCategoryName: "Printers & Scanners", description: "Flatbed scanner - 9.600 × 9.600 dpi - 216 x 297 mm - Hi-Speed USB - Bluetooth", price: 129),

    Product(image: UIImage(), name: "Notebook Professional 17", id: "HT-1011", mainCategoryName: "Computer Systems", description: "Notebook Professional 17 with 2,80 GHz quad core, 17\" Multitouch LCD, 8 GB DDR3 RAM, 500 GB SSD - DVD-Writer (DVD-R/+R/-RW/-RAM),Windows 8 Pro", price: 2299),

    Product(image: UIImage(), name: "Notebook Basic 15", id: "HT-1000", mainCategoryName: "Computer Systems", description: "Notebook Basic 15 with 2,80 GHz quad core, 15\" LCD, 4 GB DDR3 RAM, 500 GB Hard Disc, Windows 8 Pro", price: 956),

    Product(image: UIImage(), name: "Cepat Tablet 8", id: "HT-1258", mainCategoryName: "Smartphones & Tablets", description: "8-inch Multitouch HD Screen (2000 x 1500) 32GB Internal Memory, Wireless N Wi-Fi, Bluetooth, GPS Enabled, 1.5 GHz Quad-Core Processor", price: 529),

    Product(image: UIImage(), name: "Laser Professional Eco", id: "HT-1040", mainCategoryName: "Printers & Scanners", description: "Print 2400 dpi image quality color documents at speeds of up to 32 ppm (color) or 36 ppm (monochrome), letter/A4. Powerful 500 MHz processor, 512MB of memory", price: 830)
    ]

    }

    func setupCategoryGroups() {

    categoryGroup.items = [
    categoryItemComputerSystems,
    categoryItemPrintersScanners,
    categoryItemSmartphonesTablets
    ]

    categoryGroup.isMutuallyExclusive = true
    categoryGroup.allowsEmptySelection = true
    }

    func setupFilterFeedback() {

    let frameFilterFeedback = CGRect(x: 0, y: 0, width: tableView.frame.width, height: 44)

    let filterFeedbackControl = FUIFilterFeedbackControl(frame: frameFilterFeedback)
    filterFeedbackControl.filterGroups = [categoryGroup]
    filterFeedbackControl.filterResultsUpdater = self

    tableView.tableHeaderView = filterFeedbackControl
    }

    // MARK: - Table view data source and delegate
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return productsToDisplay.count
    }

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

    let objectCell = tableView.dequeueReusableCell(
    withIdentifier: FUIObjectTableViewCell.reuseIdentifier,
    for: indexPath as IndexPath) as! FUIObjectTableViewCell

    guard indexPath.row < productsToDisplay.count else {
    return objectCell
    }

    let product = productsToDisplay[indexPath.row]

    objectCell.splitPercent = CGFloat(0.3)

    objectCell.detailImage = product.image
    objectCell.detailImage?.accessibilityIdentifier = product.name

    objectCell.headlineText = product.name
    objectCell.subheadlineText = product.id
    objectCell.footnoteText = product.mainCategoryName
    objectCell.descriptionText = product.description
    objectCell.statusText = "\(product.price) \(currency)"

    return objectCell
    }
    }

    extension FilterFeedbackExample: FUIFilterResultsUpdating {

    func updateFilterResults(for filterFeedbackControl: FUIFilterFeedbackControl) {

    // reset the products to their initial state
    // this means no filters are applied
    // and the products are sorted by name
    productsToDisplay.removeAll()

    let activeFilterItems = filterFeedbackControl.filterItems.filter({ $0.isActive })

    if !activeFilterItems.isEmpty {
    // search if one of the filters is contained in the activeFilters
    // and apply it, then break out of the loop
    // because only one category filter can be active
    for filterItem in categoryGroup.items {
    if activeFilterItems.contains(filterItem) {
    let filteredProducts = products.filter { product in
    product.mainCategoryName == filterItem.title
    }
    productsToDisplay.append(contentsOf: filteredProducts)
    }
    }
    } else {
    productsToDisplay = products
    }

    tableView.reloadData()
    }
    }


  8. Select the Table View Controller and specify that its Custom Class is FilterFeedbackExample.

  9. Run the app to have a running example of a FilterFeedback control.


Using and Enhancing a Map


The following instructions demonstrate how to add a map to an application that opens in Satellite mode, centers on the user's current location and then adds points of interest and a toolbar using FUIMarkerAnnotationView and FUIMapDetailPanel. The following article may also be of use when learning about MapKit.
MapKit Tutorial: Getting Started

  1. In either the FilterSample app or the GettingStarted app, add a new View Controller and drag the storyboard entry point to point at the newly added screen.

  2. Drag a Map Kit View onto the newly added view controller.

  3. Drag each edge of the Map Kit View to the edges of the parent view controller. Then click on the Add New Constraints button. Set each value to 0 and enable each one by clicking on each red I indicator. Finally click on Add 4 Constraints.

  4. Specify the options of the map such as those shown below.  Note, you may see a crash if the traffic option is checked in combination with Satellite.

  5. Create a new file of type Cocoa Touch Class named MapViewController that extends UIViewController.

  6. Set the View Controller's custom class to be MapViewController.

  7. Replace the contents of MapViewController.swift with the following code which will zoom the map in to the current user's location the first time that the user's location becomes available. For further information see CLLocationManagerDelegate.
    import UIKit
    import MapKit

    class MapViewController: UIViewController, CLLocationManagerDelegate {
    let locationManager = CLLocationManager()
    var firstTime:Bool = true

    override func viewDidLoad() {
    super.viewDidLoad()

    locationManager.delegate = self
    locationManager.requestWhenInUseAuthorization()
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.startUpdatingLocation()
    }

    func locationManager(_ manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]) {
    let location:CLLocation = locations.last!

    let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
    let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
    if firstTime {
    mapView.setRegion(region, animated: true)
    firstTime = false
    }
    }
    }


  8. Create an outlet named mapView by control dragging from the Map View to the MapViewController file. The outlet will be used to specify the map area to show.


  9. Edit the Info.plist file and add a key value for Privacy - Location When In Use Usage Description.

  10. Run it and notice that a map is shown.

    Note, if you are using the simulator, the location can be set via the Debug > Location menu.

  11. Now let's enhance the map by adding some locations on the map with the help of some controls from Fiori for iOS (icons for the annotations and a FUIDetailsPanelContainer showing a selectable list of annotations).

  12. Duplicate MapViewController.swift (select the file and from the File menu choose Duplicate...) and rename the backup to MapViewController.old.

  13. Replace the contents of MapViewController.swift with the following code taken from the SAP Fiori Mentor app for the FUIMarkerAnnotationView and FUIMapDetailPanel.
    import SAPFiori
    import MapKit

    class MapViewController: UIViewController {


    @IBOutlet weak var mapView: MKMapView!

    let latitudinalMeters = 300_000.0
    let longitudinalMeters = 300_000.0

    var dansCampingLocations: [Location] {
    get {
    let location1 = Location(name: "Long Point Provincial park",
    address: "Norfolk County, ON N0E 1M0",
    latitude: 42.5788954,
    longitude: -80.38762,
    country: "Canada",
    region: "North America")

    let location2 = Location(name: "Killarney Provincial Park",
    address: "960 ON-637, Killarney, ON P0M 2A0",
    latitude: 46.0133468,
    longitude: -81.4091228,
    country: "Canada",
    region: "North America")

    let location3 = Location(name: "Bruce Peninsula National Park",
    address: "469 Cyprus Lake Rd, Tobermory, ON N0H 2R0",
    latitude: 45.2113526,
    longitude: -81.6714036,
    country: "Canada",
    region: "North America")

    let location4 = Location(name: "Beausoleil Island",
    address: "2611 Honey Harbour Rd, Honey Harbour, ON P0E 1E0",
    latitude: 44.8701488,
    longitude: -79.875788,
    country: "Canada",
    region: "North America")

    return [location1, location2, location3, location4]
    }
    }

    var locationsInTableView = [Location]()

    var container: FUIMapDetailPanel?

    override func viewDidLoad() {
    super.viewDidLoad()
    setupDetailPanel()
    locationsInTableView = dansCampingLocations
    }

    override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    }

    override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    //set initial location in SAP Waterloo:
    let initialLocation = CLLocationCoordinate2D(latitude: 44.4804246, longitude: -80.5528764)
    centerMap(on: initialLocation)

    //Optional code to change the map annotations (icon and color)
    class FioriMarker: FUIMarkerAnnotationView {
    override var annotation: MKAnnotation? {
    willSet {
    markerTintColor = .preferredFioriColor(forStyle: .map2)
    glyphImage = FUIIconLibrary.map.marker.walk.withRenderingMode(.alwaysTemplate)

    //FUIIconLibrary.map.marker.walk

    displayPriority = .defaultHigh
    }
    }
    }
    mapView.register(FioriMarker.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

    for location in locationsInTableView {
    mapView.addAnnotation(location.pointAnnotation())
    }

    DispatchQueue.main.async {
    self.container!.presentContainer()
    }
    }

    private func setupDetailPanel() {
    container = FUIMapDetailPanel(parentViewController: self, mapView: mapView)
    if let container = container {
    container.isSearchEnabled = true
    container.searchResults.tableView.dataSource = self
    container.searchResults.tableView.delegate = self
    container.searchResults.tableView.register(FUIMapDetailTagObjectTableViewCell.self, forCellReuseIdentifier: FUIMapDetailTagObjectTableViewCell.reuseIdentifier)
    container.searchResults.tableView.separatorStyle = .singleLine

    container.searchResults.searchBar.delegate = self
    }
    }

    private func centerMap(on location: CLLocationCoordinate2D) {
    let coordinateRegion = MKCoordinateRegion(center: location,
    latitudinalMeters: latitudinalMeters,
    longitudinalMeters: longitudinalMeters)
    mapView.setRegion(coordinateRegion, animated: true)
    }
    }

    extension MapViewController: UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    if searchText.isEmpty {
    locationsInTableView = dansCampingLocations
    } else {
    locationsInTableView = dansCampingLocations.filter { location in
    location.name.localizedCaseInsensitiveContains(searchText) ||
    location.address.localizedCaseInsensitiveContains(searchText) ||
    location.region.localizedCaseInsensitiveContains(searchText) ||
    location.country.localizedCaseInsensitiveContains(searchText)
    }
    }
    container?.searchResults.tableView.reloadData()
    }
    }

    extension MapViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return locationsInTableView.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: FUIMapDetailTagObjectTableViewCell.reuseIdentifier,
    for: indexPath as IndexPath)
    guard let objectCell = cell as? FUIMapDetailTagObjectTableViewCell else {
    return cell
    }

    let location = locationsInTableView[indexPath.row]
    objectCell.splitPercent = CGFloat(0.97)
    objectCell.headlineText = location.name
    objectCell.subheadlineText = location.address
    objectCell.subheadlineLabel.numberOfLines = 2
    return objectCell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let selectedAnnotation = locationsInTableView[indexPath.row].pointAnnotation()

    centerMap(on: selectedAnnotation.coordinate)
    }
    }

    class Location {
    let name: String
    let address: String
    let latitude: Double
    let longitude: Double
    let country: String
    let region: String

    init(name: String, address: String, latitude: Double, longitude: Double, country: String, region: String) {
    self.name = name
    self.address = address
    self.latitude = latitude
    self.longitude = longitude
    self.country = country
    self.region = region
    }

    private var coordinate: CLLocationCoordinate2D {
    get {
    return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }
    }

    public func pointAnnotation() -> MKPointAnnotation {
    let point = MKPointAnnotation()
    point.coordinate = coordinate
    point.title = name
    point.subtitle = region
    return point
    }
    }​


  14. Run it and notice that a map appears containing a few of my favorite camping locations.



Previous (Offline OData)   Home