Skip to Content

Build a Product List

test
0 %
Build a Product List
Details

Build a Product List

June 1, 2020
Created by
April 13, 2020
Build an entity list using SAP Cloud Platfrom SDK for iOS controls, with storyboard segues for navigation between the overview screen and the product list.

You will learn

  • How to use storyboard segues to navigate between screens
  • How to prepare a segue to set the title of the destination screen of each navigation

Prerequisites

  • Development environment: Apple Mac running macOS Catalina or higher with Xcode 11 or higher
  • SAP Cloud Platform SDK for iOS: Version 5.0 or higher


Step 1: Create the product list screen

The Overview screen allows the user to navigate to a list of products or customers using the FUITableViewHeaderFooterView. Both lists are implemented almost exactly the same.

In this tutorial, you will implement the product list first, a FUISearchBar to let the user search for entities in the corresponding list, and navigation from the OverviewTableViewController to the product list screen.

  1. Open the Main.storyboard and use the Object Library like before to drag a new Table View Controller to the storyboard right next to the Overview Table View Controller.

    Main Storyboard Product List
  2. Create a new Cocoa Touch Class using the Project Navigator TutorialApp > New File ... > Cocoa Touch Class . Make sure the new class is inheriting from UITableViewController and give it the name ProductsTableViewController.

    Main Storyboard Product List
  3. Go back to the Main.storyboard and set the Custom Class of the newly added Table View Controller to ProductsTableViewController.

    Main Storyboard Product List

    To enable the user to navigate between screens and passing data between view controllers you can use Storyboard Segues to you advantage. Segues can be created in Interface Builder and are accessible in the View Controllers class through the override prepareForSegue(for:sender:) method.

  4. You should be still in the Main.storyboard, there select the OverviewTableViewController and control + drag to the ProductsTableViewController.

    Main Storyboard Product List

    If you look closely, you can see that the ProductsTableViewController has a Navigation Item now allowing your user to navigate back to the Overview Screen. You get the back navigation out of the box because you’ve embedded the OverviewTableViewController in a Navigation Controller. The arrow between the View Controllers indicates a segue, for you to be able to distinguish multiple segues from each other they, like the Table View Cells, need an identifier.

  5. Select the segue in Interface Builder and click on the Attributes Inspector. As identifier enter showProductsList and hit return.

    Main Storyboard Product List
Log on to answer question
Step 2: Implement a prepare for segue method

For the product list, it is not necessary to pass any crucial data in, but we want to set the navigation item’s title before finishing up the navigation.

You can store the segue identifier in a class property for cleaner code and use it in the prepareForSegue(for:Sender:) method.

  1. Open the OverviewTableViewController.swift class and add the following class property:

    private let productSegueIdentifier = "showProductsList"
    
    
  2. Before the closing class bracket, add the prepareForSegue(for:Sender:) method which will be called by the system right before the navigation finishes it’s completion and the destination View Controller is loaded into memory.

    /**
    In a storyboard-based application, you will often want to do a little preparation before navigation.
    Using a Switch-statement let's you distinct between the different segues. Right now there is only the showProductsList but we will add a showCustomersList later on.
    */
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        switch segue.identifier {
        case productSegueIdentifier:
            let productsTableVC = segue.destination as! ProductsTableViewController
            productsTableVC.navigationItem.title = NSLocalizedString("All Products (\(products.count))", comment: "")
        default:
            return
        }
    }
    
    

    Theoretically a segue gets performed without you having to do any extra call in code, because our FUITableViewHeaderFooterView has it’s own change handler we have to call performSegue(withIdentifier:).

  3. Locate the tableView(_:viewForFooterInSection:) method and change the code to call the needed method:

    override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        if section == 1 {
            let headerFooterView = tableView.dequeueReusableHeaderFooterView(withIdentifier: FUITableViewHeaderFooterView.reuseIdentifier) as! FUITableViewHeaderFooterView
            headerFooterView.didSelectHandler = {
                // perform the segue with the defined identifier if the user taps on the See All footer view.
                self.performSegue(withIdentifier: self.productSegueIdentifier, sender: self)
            }
            headerFooterView.style = .attribute
            headerFooterView.titleLabel.text = NSLocalizedString("See All", comment: "")
            headerFooterView.attributeLabel.text = "\(products.count)"
            return headerFooterView
        } else {
            return UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        }
    }
    
    
  4. If you run the app now you can navigate between Overview Screen and Product List Screen.

    Main Storyboard Product List
Log on to answer question
Step 3: Implement a product list

The Product List is a Table View Controller which means the structure is similar to the Overview Table View Controller with the difference that you won’t use any FUITableViewHeaderFooterView.

  1. Open the ProductsTableViewController.swift class and add the following import statements:

    import SAPFiori
    import SAPFoundation
    import SAPOData
    import SAPFioriFlows
    import SAPCommon
    
    
  2. Now we will add parts of the class properties we already have used in the Overview Table View Controller. Implement the following lines of code at the top of the class:

    let destinations = FileConfigurationProvider("AppParameters").provideConfiguration().configuration["Destinations"] as! NSDictionary
    
    var dataService: ESPMContainer<OnlineODataProvider>? {
        guard let odataController = OnboardingSessionManager.shared.onboardingSession?.odataControllers[destinations["com.sap.edm.sampleservice.v2"] as! String] as? Comsapedmsampleservicev2OnlineODataController, let dataService = odataController.espmContainer else {
            AlertHelper.displayAlert(with: NSLocalizedString("OData service is not reachable, please onboard again.", comment: ""), error: nil, viewController: self)
            return nil
        }
        return dataService
    }
    
    private let appDelegate = UIApplication.shared.delegate as! AppDelegate
    private let logger = Logger.shared(named: "ProductsTableViewController")
    
    private var imageCache = [String: UIImage]()
    private var productImageURLs = [String]()
    private var products = [Product]()
    
    

    You might wonder why we are not passing the data service in from the Overview. Later on when adapting the app to work on MacOS through Mac Catalyst, the user will have the option to jump directly into the product list. In that case you won’t perform a segue and so on not be able to pass in the data service. Of course there are ways to refactor this into a more centralized way but for simplicity reason we stick to this approach.

  3. Now implement the viewDidLoad() to register the needed cells and setup the table view:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
        tableView.estimatedRowHeight = 120
        tableView.rowHeight = UITableView.automaticDimension
    }
    
    
  4. We will use the SAPFioriLoadingIndicator for this table view controller as well. Let your class conform to the SAPFioriLoadingIndicator protocol:

    class ProductsTableViewController: UITableViewController, SAPFioriLoadingIndicator {
        var loadingIndicator: FUILoadingIndicatorView?
    
    ///...
    
    }
    
    

    Next you will implement the Table View’s data source methods almost identically to the ones in the Overview.

    In case you don’t have the Runtime Root URL anymore you can find it in Mobile Services, select your app configuration in the Native/Hybrid screen. There you click on Mobile Sample OData ESPM in the Assigned Features section. The detail screen for the Mobile Sample OData ESPM will open. There you find the Runtime Root URL for this service, copy the whole URL.

  5. Add the following lines of code below the closing bracket of the viewDidLoad() method:

    override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return products.count
        }
    
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let product = products[indexPath.row]
            let productCell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier) as! FUIObjectTableViewCell
            productCell.accessoryType = .detailDisclosureButton
            productCell.headlineText = product.name ?? "-"
            productCell.subheadlineText = product.categoryName ?? "-"
            productCell.footnoteText = product.stockDetails?.quantity?.intValue() != 0 ? NSLocalizedString("In Stock", comment: "") : NSLocalizedString("Out", comment: "")
            // set a placeholder image
            productCell.detailImageView.image = FUIIconLibrary.system.imageLibrary
    
            // This URL is found in Mobile Services
            let baseURL = <YOUR URL>
            let url = URL(string: baseURL.appending(productImageURLs[indexPath.row]))
    
            guard let unwrapped = url else {
                logger.info("URL for product image is nil. Returning cell without image.")
                return productCell
            }
            // check if the image is already in the cache
            if let img = imageCache[unwrapped.absoluteString] {
                productCell.detailImageView.image = img
            } else {
                // The image is not cached yet, so download it.
                loadImageFrom(unwrapped) { image in
                    productCell.detailImageView.image = image
                }
            }
            // Only visible on regular
            productCell.descriptionText = product.longDescription ?? ""
    
            return productCell
        }
    
    
  6. Inside the just implemented method assign the copied URL to the baseURL instead of <YOUR URL> placeholder.

    The code won’t compile yet because the data loading methods are missing.

  7. Implement the data loading methods between the data source methods and the viewDidLoad() similar to the Overview. The difference is that you only have one backend call and so you don’t need a Dispatch Group.


    private func loadData() { showFioriLoadingIndicator() fetchProducts { self.tableView.reloadData() self.hideFioriLoadingIndicator() } } /** Fetch the products and handle it's errors. In case of success set the data and call the completion handler so you can stop the loading indicator. */ private func fetchProducts(completionHandler: @escaping () -> Void) { dataService?.fetchProducts() { [weak self] result, error in if let error = error { AlertHelper.displayAlert(with: NSLocalizedString("Failed to load list of products!", comment: ""), error: error, viewController: self!) self?.logger.error("Failed to load list of products!", error: error) return } self?.products.append(contentsOf: result!) self?.productImageURLs.append(contentsOf: result!.map { $0.pictureUrl ?? "" }) completionHandler() } } private func loadImageFrom(_ url: URL, completionHandler: @escaping (_ image: UIImage) -> Void) { let appDelegate = UIApplication.shared.delegate as! AppDelegate if let sapURLSession = appDelegate.sessionManager.onboardingSession?.sapURLSession { sapURLSession.dataTask(with: url, completionHandler: { data, _, error in if let error = error { self.logger.error("Failed to load image!", error: error) return } if let image = UIImage(data: data!) { // safe image in image cache self.imageCache[url.absoluteString] = image DispatchQueue.main.async { completionHandler(image) } } }).resume() } }
  8. Before running the app make sure to call loadData() within the viewDidLoad() as a last statement:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
        tableView.estimatedRowHeight = 120
        tableView.rowHeight = UITableView.automaticDimension
    
        loadData()
    }
    
    
    Main Storyboard Product List
Log on to answer question
Step 4: Implement a search bar

The SAP Fiori for iOS Search Bar control inherits is using the standard UISearchBar inside but enhances the whole search controller with a barcode reader. We’re not going to implement the barcode reader in this tutorial series but if you’re interested in how to do so take a look at the Use the Barcode Scanner API tutorial at a later point.

  1. In order to add a search bar to the view you need a FUISearchController instance, implement the following two class properties to hold on an instance of the search controller as well as the search results.

    private var searchController: FUISearchController?
    private var searchedProducts = [Product]()
    
    
  2. Next implement a setup method to instantiate the FUISearchController and do some setup. Add the following method below the tableView(_:cellForRowAt:) method:

    private func setupSearchBar() {
        // Search Controller setup
        searchController = FUISearchController(searchResultsController: nil)
    
        // Handle the search result directly in the ProductsTableViewController
        searchController!.searchResultsUpdater = self
        searchController!.hidesNavigationBarDuringPresentation = false
        searchController!.searchBar.placeholderText = NSLocalizedString("Search for products...", comment: "")
        searchController!.searchBar.isBarcodeScannerEnabled = false
    
        // Set the search bar to the table header view like we did with the KPI Header.
        self.tableView.tableHeaderView = searchController!.searchBar
    }
    
    
  3. The code won’t compile at the moment because you haven’t conformed to the UISearchResultsUpdating protocol. We will fix that at a later point but first call the setupSearchBar() method right below the loadData() call in the viewDidLoad() method.

    override func viewDidLoad() {
        super.viewDidLoad()
    
        tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
        tableView.estimatedRowHeight = 120
        tableView.rowHeight = UITableView.automaticDimension
    
        loadData()
        setupSearchBar()
    }
    
    
  4. To fix the compile time error create an extension conforming to the UISearchResultsUpdating protocol:

    // MARK: - UISearchResultsUpdating extension
    
    extension ProductsTableViewController: UISearchResultsUpdating {
        func updateSearchResults(for searchController: UISearchController) {
            if let searchText = searchController.searchBar.text {
                // TODO: Implement
                return
            }
        }
    }
    
    

    We will go ahead and implement that later. For now leave it as is and we proceed implementing the search logic.
    To do so we implement some helper methods deciding if the text in the search field is empty, if the user is actually in the process of searching but also the search logic itself.

  5. First implement the method to check if the search field is empty or not. Add the method below the setupSearchBar() method:

    // Verify if the search text is empty or not
    private func searchTextIsEmpty() -> Bool {
       return searchController?.searchBar.text?.isEmpty ?? true
    }
    
    
  6. Implement a method directly below deciding if the user is currently searching or not:

    // Verify if the user is currently searching or not
    private func isSearching() -> Bool {
        return searchController?.isActive ?? false && !searchTextIsEmpty()
    }
    
    
  7. Lastly implement the search logic method responsible for actually searching through the products and returning a list of the searched for products:

    // actual search logic for finding the correct products for the term the user is searching for
    private func searchProducts(_ searchText: String) {
        searchedProducts = products.filter({( product : Product) -> Bool in
            // Make sure the string is completely lower-cased or upper-cased. Either way makes it easier for you to
            // compare strings.
            return product.name?.lowercased().contains(searchText.lowercased()) ?? false
        })
    
        // Don't forget to trigger a reload.
        tableView.reloadData()
    }
    
    

    With the search logic implemented we can go ahead and fully implement the extension we defined before.

  8. Replace the updateSearchResults(for:) method in the UISearchResultsUpdating extension:

    extension ProductsTableViewController: UISearchResultsUpdating {
        func updateSearchResults(for searchController: UISearchController) {
            if let searchText = searchController.searchBar.text {
    
                // Simply call the search logic method and pass the searched for text here!
                // You could check if the search text's length is at least 3 characters
                // to not trigger the search for each and every single character.
                // if searchText.count >= 3 { searchProducts(searchText) }
    
                searchProducts(searchText)
                return
            }
        }
    }
    
    
  9. Run the app now and you should be able to search for products.

    Main Storyboard Product List
What protocol conformance is necessary to handle search inputs of the user? Please enter the protocol name below.
×

Next Steps

Back to top