Getting Started with the SAP Cloud Platform SDK for iOS – Part 6 – SAP Fiori for iOS
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.
- The SAP Fiori Mentor App can be downloaded from the App Store onto an iPad.
- Under the UI Components section of the app, select the Toast Message component.
- The control’s settings can be modified as shown below.
- Sample code is shown by pressing the following button.
- 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)
- Run the app and press the button to call getProductsOffline. Notice the result is now shown in a toast.
- 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() } }
- 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.
- Under the UI Components section of the SAP Fiori Mentor app, select the Filter Feedback component.
- 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.
- The source code to try out the control is available by pressing the following button.
- Create a new project named FilterSample.
- Add the embedded binaries for the SAP Cloud Platform SDK for iOS.
- Add a new Table View Controller and drag the storyboard entry point to point at the newly added screen.
- 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() } }
- Select the Table View Controller and specify that its Custom Class is FilterFeedbackExample.
- 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
- 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.
- Drag a Map Kit View onto the newly added view controller.
- 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.
- 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.
- Create a new file of type Cocoa Touch Class named MapViewController that extends UIViewController.
- Set the View Controller’s custom class to be MapViewController.
- 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 } } }
- 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.
- Edit the Info.plist file and add a key value for Privacy – Location When In Use Usage Description.
- 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. - 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).
- Duplicate MapViewController.swift (select the file and from the File menu choose Duplicate…) and rename the backup to MapViewController.old.
- 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 } }
- Run it and notice that a map appears containing a few of my favorite camping locations.