Skip to Content

Create a List Report Floorplan

Use the SAP BTP SDK for iOS to build a simple List Report Floorplan containing an FUISearchBar
You will learn
qualitureRobin van het HofMarch 31, 2022
Created by
qualiture
April 20, 2017
Contributors
qualiture

Prerequisites

  • How to use the SAPFiori UI controls to build a List Report Floorplan
  • Step 1

    In this example, you build upon the Tutorial app created using the Sample OData service. If you examine the service’s metadata you can see entity Supplier has a one-to-many relationship with Products:

    XML
    Copy
    <EntityType Name="Supplier">
      <Key>
        <PropertyRef Name="SupplierId"/>
      </Key>
      <Property MaxLength="40" Name="City" Nullable="true" Type="Edm.String"/>
      <Property MaxLength="3" Name="Country" Nullable="true" Type="Edm.String"/>
      <Property MaxLength="255" Name="EmailAddress" Nullable="true" Type="Edm.String"/>
      <Property MaxLength="10" Name="HouseNumber" Nullable="true" Type="Edm.String"/>
      <Property MaxLength="30" Name="PhoneNumber" Nullable="true" Type="Edm.String"/>
      <Property MaxLength="10" Name="PostalCode" Nullable="true" Type="Edm.String"/>
      <Property MaxLength="60" Name="Street" Nullable="true" Type="Edm.String"/>
      <Property MaxLength="10" Name="SupplierId" Nullable="false" Type="Edm.String"/>
      <Property MaxLength="80" Name="SupplierName" Nullable="true" Type="Edm.String"/>
      <Property Name="UpdatedTimestamp" Type="Edm.DateTime"/>
      <NavigationProperty FromRole="Supplier" Name="Products" Relationship="ESPM.Supplier_Product_One_Many0" ToRole="Product"/>
    </EntityType>
    

    The app you’ve generated with the SAP BTP SDK Assistant for iOS (Assistant) has currently its UI. You’re going to make that app your own now.

    Open the Main.storyboard, select all displayed View Controllers and delete them.

    Change UI

    Add a new UITableViewController to the storyboard and embed it in a UINavigationController.

    Change UI

    Now change the UINavigationController to be the Initial View Controller.

    Change UI

    To let the app load your newly added screen it is necessary to change the application screen code in the ApplicationUIManager.swift.

    Open the ApplicationUIManager.swift class and locate the showApplicationScreen(completionHandler:) method.

    Change UI

    Replace the following code inside the showApplicationScreen(completionHandler:) method:

    Swift
    Copy
      let appDelegate = (UIApplication.shared.delegate as! AppDelegate)
      let splitViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "MainSplitViewController") as! UISplitViewController
      splitViewController.delegate = appDelegate
      splitViewController.modalPresentationStyle = .currentContext
      splitViewController.preferredDisplayMode = .allVisible
      appViewController = splitViewController
    
    

    with:

    Swift
    Copy
       let navigationViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController() as! UINavigationController
       appViewController = navigationViewController
    
    

    Instead of instantiating the MainSplitViewController the UIStoryboard will instantiate the initial View Controller and cast it to the UINavigationController.

    Lastly, you have to create a new class inheriting from UITableViewController. Create a new Cocoa Touch Class and name it SupplierTableViewController.

    Change UI

    Set the created class as Custom Class in the storyboard for the added UITableViewController.

    Change UI
  • Step 2

    Now that you have the first UITableViewController setup you will add code to load and display suppliers in a UITableView.

    Open the SupplierTableViewController.swift class and add the following import statements for full usage of the SDK:

    Swift
    Copy
    import UIKit
    import SAPOData
    import SAPFoundation
    import SAPFiori
    import SAPFioriFlows
    import SAPCommon
    import SAPOfflineOData
    import ESPMContainerFmwk
    import SharedFmwk
    
    

    Next let the SupplierTableViewController.swift class conforms to the SAPFioriLoadingIndicator protocol provided by the Assistant generated code. You can locate the protocol in the Utils group.

    Swift
    Copy
    class SupplierTableViewController: UITableViewController, SAPFioriLoadingIndicator { ... }
    
    

    Xcode will ask you to fully conform to the protocol by adding a loadingIndicator class property. Add the following line of code directly below the class declaration:

    Swift
    Copy
    var loadingIndicator: FUILoadingIndicatorView?
    
    

    Implement the following lines of code directly below the loading indicator property:

    Swift
    Copy
    // The available destinations from Mobile Services are hold in the FileConfigurationProvider. Retrieve it to find the correct data service
    let destinations = FileConfigurationProvider("AppParameters").provideConfiguration().configuration["Destinations"] as! NSDictionary
    
    // Retrieve the data service using the destinations dictionary and return it.
    let destinations = FileConfigurationProvider("AppParameters").provideConfiguration().configuration["Destinations"] as! NSDictionary
    
    var dataService: ESPMContainer<OfflineODataProvider>? {
        guard let odataController = OnboardingSessionManager.shared.onboardingSession?.odataControllers[ODataContainerType.eSPMContainer.description] as? ESPMContainerOfflineODataController, let dataService = odataController.dataService else {
            AlertHelper.displayAlert(with: "OData service is not reachable, please onboard again.", error: nil, viewController: self)
            return nil
        }
        return dataService
    }
    
    

    In case you’re using an OfflineODataProvider you have to change the above-mentioned code to use OfflineODataProvider instead of OnlineODataProvider. You have to also import SAPOfflineOData in addition to the SAPOData framework.

    SAP offers a simple-to-use Logging API with the SAPCommon framework.

    Implement the following line of code below the data service declaration:

    Swift
    Copy
    private let logger = Logger.shared(named: "SupplierTableViewController")
    
    

    Before you will implement the data loading methods you have to implement a supplier array property to save the loaded data in memory.

    Add the following line of code below the logger instantiation:

    Swift
    Copy
    private var suppliers = [Supplier]()
    
    

    From now on bigger code blocks are explained with inline comments. Read the inline comments carefully to fully understand what the code is doing and why you’re implementing it.

    Loading all available suppliers is fairly easy using the generated data service. The generated code will handle all authentication and authorization challenges for you and the data service will construct all necessary requests to load, create and update entities in your backend.

    Implement the following methods directly under the closing bracket of the viewDidLoad() method:

    Swift
    Copy
    // MARK: - Data loading
    
    /// When this method gets called to show a loading indicator. When the completion handler of the loadData(:) method gets executed hide the loading indicator.
    private func updateTableView() {
        self.showFioriLoadingIndicator()
        loadData {
            self.hideFioriLoadingIndicator()
        }
    }
    
    /// Load the suppliers by using the fetchSuppliers() method provided by the data service.
    private func loadData(completionHandler: @escaping () -> Void) {
        // fetch products
        dataService?.fetchSuppliers() { [weak self] suppliers, error in
            // handle errors
            if let error = error {
                self?.logger.error("Error while fetching list of suppliers.", error: error)
                return
            }
    
            // set loaded suppliers to property and reload data on the table view
            self?.suppliers = suppliers!
            self?.tableView.reloadData()
            completionHandler()
        }
    }
    
    

    Call the updateTableView() method inside the viewDidLoad():

    Swift
    Copy
    override func viewDidLoad() {
        super.viewDidLoad()
    
        updateTableView()
    }
    
    

    Congratulations, your app is fetching data now.

  • Step 3

    Using the UITableViewController makes it easy for a developer to display data in a list.

    First, implement two necessary UITableViewDataSource methods which are responsible for telling the UITableView how many sections and rows to display. Implement those two methods directly below the loadData(:) method:

    Swift
    Copy
    // MARK: - Table view data source
    
    /// Only one section is displayed for this screen, return 1.
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    /// Return the count of the suppliers array.
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return suppliers.count
    }
    
    

    Using the SAPFiori framework allows you to choose from a large variety of UITableViewCell classes.
    Because you’re going to display suppliers, and those have a name, an address and probably contact data the FUIContactCell would be a perfect fit here. You can always use the SAP Fiori Mentor app, available in the App Store for iPad, to get an introduction to the control.

    Before implementing the tableView(_cellForRowAt:) method responsible for dequeuing reusable cells and returning them to the UITableView, You need to register the FUIContactCell with the UITableView first. This is usually done in the viewDidLoad() method.

    Implement the following lines of code before the updateTableView() method call in the viewDidLoad():

    Swift
    Copy
    // Register the cell with the provided convenience reuse identifier.
    tableView.register(FUIContactCell.self, forCellReuseIdentifier: FUIContactCell.reuseIdentifier)
    
    // Set the separator style of the table view to none and the background colour to the standard Fiori background base colour.
    tableView.separatorStyle = .none
    tableView.backgroundColor = .preferredFioriColor(forStyle: .backgroundBase)
    
    

    It is time to implement the tableView(_cellForRowAt:) method to dequeue the FUIContactCell:

    Swift
    Copy
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
      // Get the specific supplier for the current row.
      let supplier = suppliers[indexPath.row]
    
      // Dequeue the FUIContactCell using the convenience reuse identifier. Force cast it to the FUIContactCell
      let cell = tableView.dequeueReusableCell(withIdentifier: FUIContactCell.reuseIdentifier) as! FUIContactCell
    
      // Set values to the cell's properties.
      cell.headlineText = supplier.supplierName ?? "No Name available!"
      cell.subheadlineText = "\(supplier.street ?? "") \(supplier.houseNumber ?? "") \(supplier.city ?? ""), \(supplier.postalCode ?? "") \(supplier.country ?? "")"
    
      //Navigation will be implemented later. This cell has the disclosure indicator as accessory type indicating that navigation to the user.
      cell.accessoryType = .disclosureIndicator
    
      return cell
    }
    
    

    You could run the app now and should see a list of suppliers getting loaded and displayed.

  • Step 4

    Using the FUIContactCell allows us to use an FUIActivityControl inside of the cell to let the user contact a supplier. The FUIActivityControl documentation explains the control and its variations in more detail.

    First, you have to define which so-called FUIActivityItems you want to use in the FUIActivityControl.

    Add the following line of code below the supplier’s array:

    Swift
    Copy
    /// Define the contact possibilities for the user: messaging, phone call, and email
    private let activities = [FUIActivityItem.message, FUIActivityItem.phone, FUIActivityItem.email]
    
    

    With that, you can simply add those activities to the FUIContactCell. This specific cell carries the FUIActivityControl in its belly in is accessible through the available public API.

    Go back to the tableView(_cellForRowAt:) method and add the following lines of code directly above the assignment of the accessory type:

    Swift
    Copy
    cell.activityControl.addActivities(activities)
    cell.activityControl.maxVisibleItems = 3
    
    // The FUIActivityControl provides you two different ways of reacting to user's interaction. One would be with a change handler the other would be with a delegate. Because I don't want the communication logic being in the tableView(_cellForRowAt:) method you're using the delegation way.
    cell.activityControl.delegate = self
    
    

    To properly react to the user’s interaction with the control you will implement an extension conforming to the FUIActivityControlDelegate.

    Implement the extension:

    Swift
    Copy
    extension SupplierTableViewController: FUIActivityControlDelegate {
    
      func activityControl(_ activityControl: FUIActivityControl, didSelectActivity activityItem: FUIActivityItem) {
    
        // Use a Switch to check for the identifier and act accordingly.
        switch activityItem.identifier {
        case FUIActivityItem.message.identifier:
            AlertHelper.displayAlert(with: "Messaging supplier!", error: nil, viewController: self)
            break
        case FUIActivityItem.phone.identifier:
            AlertHelper.displayAlert(with: "Calling supplier!", error: nil, viewController: self)
            break
        case FUIActivityItem.email.identifier:
            AlertHelper.displayAlert(with: "Send email to supplier!", error: nil, viewController: self)
            break
        default:
            return
        }
      }
    }
    
    

    In this tutorial, you will not implement the actual code for doing the communication. If you’re interested in how to do so, you can use Apple’s official documentation:

    If you run the app now you should see the following screen:

    Supplier List

    Tapping on one of the FUIActivityItem will result in an alert dialogue showing up.

    Supplier List
  • Step 5

    In this step, you will implement a second UITableViewController displaying all products a supplier provides.
    For this, you will use a storyboard segue to navigate to the SupplierProductsTableViewController and pass through the selected supplier.

    In case you’re not familiar with segues please visit, and carefully read the official documentation before continuing. Using Segues

    Open up the Main.storyboard and add a new UITableViewController from the Object Library directly next to the SupplierTableViewController. Create a new segue from one of the prototype cells inside of the SupplierTableViewController to the newly added UITableViewController.

    Segue

    Select the segue and open the Identity Inspector to set the Identifier to showSupplierProducts.

    Segue

    Go back to the SupplierTableViewController and add a new class constant of type String which will hold the segue identifier.

    Swift
    Copy
    private let segueIdentifier = "showSupplierProducts"
    
    

    Create a new Cocoa Touch class with the name SupplierProductsTableViewController.

    Segue

    Set the Custom Class of the newly added UITableViewController to SupplierProductsTableViewController in the Main.storyboard.

    Segue

    Open the SupplierProductsTableViewController and add a class property that will contain the selected supplier.

    Swift
    Copy
    var supplier: Supplier!
    
    

    Next, you will implement the prepare(:for:sender:) method which is responsible for making necessary preparations before the navigation is fully executed. In our case, you will pass the selected supplier to the SupplierProductsTableViewController.

    Implement the prepare(:for:sender:) method in SupplierTableViewController:

    Swift
    Copy
    // MARK: - Navigation
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    
      // Check for the correct identifier
      if segue.identifier == segueIdentifier {
    
        // Get the selected row from the table view.
        if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
    
          // Get the destination view controller and cast it to SupplierProductsTableViewController.
          let destinationVC = segue.destination as! SupplierProductsTableViewController
    
          // Set the selected supplier.
          destinationVC.supplier = suppliers[indexPathForSelectedRow.row]
        }
      }
    }
    
    

    You can utilise the tableView(_:didSelectRowAt:) method to trigger the navigation. Implement the override method:

    Swift
    Copy
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            performSegue(withIdentifier: segueIdentifier, sender: tableView)
    }
    
    

    You can now navigate back and forth between the SupplierTableViewController and the SupplierProductsTableViewController.

  • Step 6

    This view is similar to the SupplierTableViewController but instead of fetching all products, you will fetch supplier-specific products. To achieve that, you again can utilise the OData APIs. SAPOData provides the possibility to create so-called DataQuery objects which can define typical OData arguments for a backend call.

    First, you need to make the needed import statements for that class:

    Swift
    Copy
    import SAPOData
    import SAPFoundation
    import SAPFiori
    import SAPFioriFlows
    import SAPCommon
    import ESPMContainerFmwk
    import SharedFmwk
    import SAPOfflineOData
    
    
    

    Let’s use the logger again. Add the following line of code below the supplier property:

    Swift
    Copy
    private let logger = Logger.shared(named: "SupplierProductsViewController")
    
    

    Add a couple of class properties necessary for the data service instance and the fetched products. Implement the following lines of code:

    Swift
    Copy
    // The available destinations from Mobile Services are hold in the FileConfigurationProvider. Retrieve it to find the correct data service
    let destinations = FileConfigurationProvider("AppParameters").provideConfiguration().configuration["Destinations"] as! NSDictionary
    
    var dataService: ESPMContainer<OfflineODataProvider>? {
        guard let odataController = OnboardingSessionManager.shared.onboardingSession?.odataControllers[ODataContainerType.eSPMContainer.description] as? ESPMContainerOfflineODataController, let dataService = odataController.dataService else {
            AlertHelper.displayAlert(with: "OData service is not reachable, please onboard again.", error: nil, viewController: self)
            return nil
        }
        return dataService
    }
    
    private var products = [Product]()
    
    

    Also, you want to utilise the provided loading indicator. Let the class conform to the SAPFioriLoadingIndicator protocol.

    Swift
    Copy
    class SupplierProductsTableViewController: UITableViewController, SAPFioriLoadingIndicator { ... }
    
    

    Of course, don’t forget the loading indicator class property:

    Swift
    Copy
    var loadingIndicator: FUILoadingIndicatorView?
    
    

    Let’s load some data!

    You’re using the same style you’ve used in the SupplierTableViewController. Implement the following two methods and read the inline comments carefully because you will see that you utilise the DataQuery object for making a filter as well as an expand.

    If you’re not familiar with those OData specific terms please make yourself familiar with the OData specification:

    Swift
    Copy

    // You know that one u{1F609} private func updateTableView() { self.showFioriLoadingIndicator() loadData { self.hideFioriLoadingIndicator() } } private func loadData(completionHandler: @escaping () -> Void) { // Retrieve the supplier id let supplierID = supplier.supplierID! // Create a new DataQuery object applying a filter and an expand. The filter will filter on the specific // supplier id and the expand will construct the URL in a way that the backend returns the stock details // for each product. let dataQuery = DataQuery().filter(Product.supplierID == supplierID).expand(Product.stockDetails) dataService?.fetchProducts(matching: dataQuery) { [weak self] products, error in // handle errors if let error = error { self?.logger.error("Error while fetching list of products for supplier: \(supplierID).", error: error) return } // set loaded products to property and reload data on the table view self?.products = products! self?.tableView.reloadData() completionHandler() } }

    Call the updateTableView() method as last statement in the viewDidLoad().

    Swift
    Copy
    override func viewDidLoad() {
        super.viewDidLoad()
    
        updateTableView()
    }
    
    

    Products contain images for each product, it would be nice to display them as well, but to do so you have to write a little bit of code to make that happen.

    First, implement a class property holding the image URLs of all products.

    Swift
    Copy
    private var productImageURLs = [String]()
    
    

    The user might want to scroll through the products even if the images are not fully loaded yet you have to implement a simple image cache as well as a placeholder image to keep the performance of the table stable.

    Add the following line of code directly below the productImageURLs:

    Swift
    Copy
    // This simple dictionary will contain all the fetched images in memory.
    private var imageCache = [String: UIImage]()
    
    

    Now implement a method responsible for fetching the product images and caching them. Add the following method right below the loadData(:) method:

    Swift
    Copy

    /// This method will take a URL and an escaping completion handler as arguments. The URL is the backend URL for your deployed sample service. private func loadProductImageFrom(_ url: URL, completionHandler: @escaping (_ image: UIImage) -> Void) { // Retrieve the SAP URLSession from the onboarding session. let appDelegate = UIApplication.shared.delegate as! AppDelegate if let sapURLSession = appDelegate.sessionManager.onboardingSession?.sapURLSession { // Create a data task, this is the same as the URLSession data task. sapURLSession.dataTask(with: url, completionHandler: { data, _, error in // Handle errors if let error = error { self.logger.error("Failed to load image!", error: error) return } // Instantiate an image from data. if let image = UIImage(data: data!) { // safe image in image cache self.imageCache[url.absoluteString] = image // Dispatch back to the main queue. DispatchQueue.main.async { completionHandler(image) } } }).resume() } }

    Now you have the foundation for fetching and caching images. You were probably wondering where the mapping from the fetched products to the product image URLs happens. You will implement that now.

    Go back to the loadData(:) method and add the following line of code directly below the product assignment. Your loadData(:) should look like this now:

    Swift
    Copy
    private func loadData(completionHandler: @escaping () -> Void) {
    
        let supplierID = supplier.supplierID!
        let dataQuery = DataQuery().filter(Product.supplierID == supplierID).expand(Product.stockDetails)
        dataService?.fetchProducts(matching: dataQuery) { [weak self] products, error in
            // handle errors
            if let error = error {
                self?.logger.error("Error while fetching list of products for supplier: \(supplierID).", error: error)
                return
            }
    
            // set loaded products to property and reload data on the table view
            self?.products = products!
    
            // Use .map to create an array of picture URLs
            self?.productImageURLs = products!.map { $0.pictureUrl ?? "" }
            self?.tableView.reloadData()
            completionHandler()
        }
    }
    
    

    Like the last time you have to register a SAPFiori cell with the UITableView, but this time it is a FUIObjectTableViewCell. Add the following lines of code to the viewDidLoad() method right before the updateTableView(:) method call.

    Swift
    Copy
    tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
    tableView.separatorStyle = .none
    tableView.backgroundColor = .preferredFioriColor(forStyle: .backgroundBase)
    
    

    As the last step, you have to implement the table views data source methods similar to the SupplierTableViewController.

    Add the following methods directly below the loadProductImageFrom(_:completionHandler:) method and make sure to modify the value of baseURL

    Swift
    Copy
    // MARK: - Table view data source
    
    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 cell = tableView.dequeueReusableCell(withIdentifier: FUIObjectTableViewCell.reuseIdentifier) as! FUIObjectTableViewCell
    
        cell.headlineText = product.name ?? ""
        cell.subheadlineText = product.categoryName ?? ""
        cell.footnoteText = product.productID ?? ""
        cell.statusText = "Min Stock: \(product.stockDetails?.minStock?.doubleValue() ?? 0.0)"
    
        // set a placeholder image
        cell.detailImageView.image = FUIIconLibrary.system.imageLibrary
    
        // This URL is found in Mobile Services API tab
        let baseURL = "Your API found in the Sample Service assigned to your mobile app configuration in MS"
        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 cell
        }
    
        // check if the image is already in the cache
        if let img = imageCache[unwrapped.absoluteString] {
            cell.detailImageView.image = img
        } else {
            // The image is not cached yet, so download it.
            loadProductImageFrom(unwrapped) { image in
                cell.detailImageView.image = image
            }
        }
    
        return cell
        }
    }
    
    

    Run the app and navigate to the SupplierProductsTableViewController.

    Supplier Product List
  • Step 7

    Wouldn’t it be cool to also have a FUISearchBar which is inheriting from UISearchBar? - Of course, it would be, so let’s implement that.

    In case you’re not familiar with the UISearchBar or UISearchController read the official documentation:

    Open the SupplierProductsTableViewController.swift class and add the following two class properties right below the products array:

    Swift
    Copy
    private var searchController: FUISearchController?
    
    // Will hold the searched for products in that array.
    private var searchedProducts = [Product]()
    
    

    Let’s implement a bit of setup logic for the FUISearchBar. Add the following method to your class:

    Swift
    Copy
    private func setupSearchBar() {
        // Search Controller setup
        searchController = FUISearchController(searchResultsController: nil)
    
        // The SupplierProductsViewController will take care of updating the search results.
        searchController!.searchResultsUpdater = self
        searchController!.hidesNavigationBarDuringPresentation = true
        searchController!.searchBar.placeholderText = "Search for products"
    
        // No Barcode scanner needed here
        searchController!.searchBar.isBarcodeScannerEnabled = false
    
        self.tableView.tableHeaderView = searchController!.searchBar
    }
    
    

    Xcode will complain now because the SupplierProductsTableViewController.swift class is not conforming to the UISearchResultsUpdating protocol.

    Add an extension to your class, like you did in the SupplierTableViewController:

    Swift
    Copy
    extension SupplierProductsTableViewController: UISearchResultsUpdating {
        func updateSearchResults(for searchController: UISearchController) {
            // to implement
        }
    }
    
    

    Call the setupSearchBar() method inside the viewDidLoad():

    Swift
    Copy
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSearchBar()
        updateTableView()
    
    }
    
    

    If you run the app now you should see the FUISearchBar being displayed above the UITableView.

    Supplier Product List

    Now you have to implement some search logic to be called in the updateSearchResults(for:) method.

    Implement the following methods right below the setupSearchBar() method and carefully read the inline comments.

    Swift
    Copy
    // verify if the search text is empty or not
    private func searchTextIsEmpty() -> Bool {
        return searchController?.searchBar.text?.isEmpty ?? true
    }
    
    // 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
            return product.name?.lowercased().contains(searchText.lowercased()) ?? false
        })
    
        tableView.reloadData()
    }
    
    // verify if the user is currently searching or not
    private func isSearching() -> Bool {
        return searchController?.isActive ?? false && !searchTextIsEmpty()
    }
    
    

    Cool! Let’s implement the updateSearchResults(for:) method:

    Swift
    Copy
    extension SupplierProductsTableViewController: UISearchResultsUpdating {
        func updateSearchResults(for searchController: UISearchController) {
            // Get the searched-for term, note here that you don't have a time bouncer which waits for the user to finish its input. You could implement that if needed, for this simple example you do life searches for each character. I wouldn't recommend doing that over a large data set.
            if let searchText = searchController.searchBar.text {
                // Feed it to the search logic.
                searchProducts(searchText)
                return
            }
        }
    }
    
    

    As our last task here is to change the UITableView data source methods to use react to potential searches by the user.

    Change the tableView(_:numberOfRowsInSection:) method to:

    Swift
    Copy
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // if the user is searching display the searched for products and all products otherwise.
        return isSearching() ? searchedProducts.count : products.count
    }
    
    

    Also change the tableView(_:cellForRowAt:) method to retrieve the product either from the product array or in case of a search from the searchedProducts array.

    Change the following line of code:

    Swift
    Copy
    let product = products[indexPath.count]
    
    

    to

    Swift
    Copy
    let product = isSearching() ? searchedProducts[indexPath.row] : products[indexPath.row]
    
    

    If you compile and run the app now you should see that you can search for products.

    Searchbar Runtime

    What protocol do you have to conform to, to handle search results? Check the correct answer.

Back to top