Skip to Content

Getting Started with the SAP Cloud Platform SDK for iOS

Part 6 — SAP Fiori for iOS

Previous (Offline OData)

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 Floorplan 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 getProducts method, replace the second print line with the following code.

    let msg = "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 getProducts. 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. In the buttonPressed method only call the offline version of getProducts by commenting out getProducts.
    //getProducts(serviceURL, myContext.sapURLSession)
    getProductsOffline(serviceURL, myContext.sapURLSession)
    
  9. 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
    
    struct Product {
        var image = UIImage()
        var name = "Product Name"
        var id = "HT-1234"
        var mainCategoryName = "Computer Systems"
        var description = "Product Description"
        var price = 499
    }
    
    class FilterFeedbackExample: UITableViewController {
        let currency = "USD"
        var products = [Product]()
        let estimatedRowHeight = CGFloat(80)
        
        var filterFeedbackControl: FUIFilterFeedbackControl!
        var sortGroup = FUIFilterGroup()
        var categoryGroup = FUIFilterGroup()
        
        let sortItemName = FUIFilterItem("Sort: Name",
                                         isFavorite: true,
                                         isActive: true)
        
        let sortItemPrice = FUIFilterItem("Sort: Price",
                                          isFavorite: true,
                                          isActive: false)
        
        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 = UITableViewAutomaticDimension
            tableView.separatorStyle = .singleLine
        
            setupProducts()
            setupSortGroups()
            setupFilterFeedback()
        }
        
        func setupProducts() {
            products = [  
                Product(image: UIImage(), name: "Photo Scan", id: "HT-1080", mainCategoryName: "Printers & Scanners", description: "Flatbed scanner - 9.600 X 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 setupSortGroups() {
            sortGroup.items = [sortItemName, sortItemPrice]
            sortGroup.isMutuallyExclusive = true
            sortGroup.allowsEmptySelection = false
            
            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 = [sortGroup, categoryGroup]
            filterFeedbackControl.filterResultsUpdater = self    
            tableView.tableHeaderView = filterFeedbackControl
        }
        
        // MARK: - Table view data source and delegate
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return products.count
        }
        
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(
                withIdentifier: FUIObjectTableViewCell.reuseIdentifier,
                for: indexPath as IndexPath)
            
            guard let objectCell = cell as? FUIObjectTableViewCell else {
                return cell
            }
            
            let product = products[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
            setupProducts()
            
            let activeFilterItems = filterFeedbackControl.filterItems.filter({ $0.isActive })
            
            // 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) {
                    products = products.filter { product in
                        product.mainCategoryName == filterItem.title
                    }
                    break
                }
            }
            
            // either name or price filter can be active but not both
            if activeFilterItems.contains(sortItemName) {
                products = products.sorted {
                    $0.name < $1.name
                }
            } else if activeFilterItems.contains(sortItemPrice) {
                products = products.sorted {
                    $0.price < $1.price
                }
            }
            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 FUIDetailPanelContainer. 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 FUIDetailPanelContainer.
    
    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: FUIDetailPanelContainer?
        
        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 = FUIDetailPanelContainer(parentViewController: self, mapView: mapView)
            if let container = container {
                container.isSearchEnabled = true
                container.searchResultsViewController.tableView.dataSource = self
                container.searchResultsViewController.tableView.delegate = self
                container.searchResultsViewController.tableView.register(FUIMapDetailTagObjectTableViewCell.self, forCellReuseIdentifier: FUIMapDetailTagObjectTableViewCell.reuseIdentifier)
                container.searchResultsViewController.tableView.separatorStyle = .singleLine
                
                container.searchResultsViewController.searchBar.delegate = self
            }
        }
        
        private func centerMap(on location: CLLocationCoordinate2D) {
            let coordinateRegion = MKCoordinateRegionMakeWithDistance(location,
                                                                      latitudinalMeters,
                                                                      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?.searchResultsViewController.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)

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply