Home Articles

Build a simple API search

This article helps to design a UITableViewController with a SearchBar and populates results whenever we start typing and follows opening a SafariWebView on tapping on the search results.

Also, it helps to understand how to use Alamofire with SwiftyJSON to hit an API and fetch some results, parse them, and show the results in an UITableViewController.

Create a new project in Xcode

Create a new project in Xcode

Integrate Pod

Setup podfile

Go to the terminal and enter the project directory path and type pod init, this creates podfile in our directory and then open this podfile from the project directory and add following pods into it


platform :ios, '9.0'

target 'WikiSearch' do  
  use_frameworks!
  pod 'Alamofire', '~> 4.7'  
  pod 'SwiftyJSON', '~> 4.0'
end

Install Pods

then run pod install in the terminal, this helps us to install these dependencies and resulting in the creation .xcworkspace file, close the project and open the WikiSearch.xcworkspace file. If you are new to cocoapods, please have a look here

Storyboard

Add a UITableViewController

Let's add a UITableViewController with NavigationController in the Main.storyboard

Adding a UITableViewController using Storyboard

Add Views

I am going to create an ImageView, TitleLabel and DescriptionLabel, so there will be three UIElements in the TableView cell.

Time to Code

Add a TableViewCell

Add a new class CustomTableViewCell sub-classing UITableViewCell to the project and add the referencing layouts from the table view cell.


import UIKit

class CustomTableViewCell: UITableViewCell {

    @IBOutlet weak var wikiImageView: UIImageView!
    
    @IBOutlet weak var titleLabel: UILabel!
    
    @IBOutlet weak var descriptionLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
}

Add an UISearchController

Now, add a new class SearchResultsTableViewController sub-classing UITableViewController and create a UISearchController property in it.


private let searchController = UISearchController(searchResultsController: nil)

Customize UISearchController

Let us customize this searchBar and background view for the tableview like below:


private func setupSearchBar() {  
    searchController.searchBar.delegate = self  
    searchController.dimsBackgroundDuringPresentation = false  
    searchController.hidesNavigationBarDuringPresentation = false  
    searchController.searchBar.placeholder = "Search any Topic"  
    definesPresentationContext = true  
    tableView.tableHeaderView = searchController.searchBar  
}

and


private func setupTableViewBackgroundView() {  
   let backgroundViewLabel = UILabel(frame: .zero)  
   backgroundViewLabel.textColor = .darkGray  
   backgroundViewLabel.numberOfLines = 0  
   backgroundViewLabel.text =   
          "Oops, /n No results to show! ..."  
   tableView.backgroundView = backgroundViewLabel  
}

add these methods into viewDidLoad of the controller.


override func viewDidLoad() {  
    super.viewDidLoad()  
    tableView.tableFooterView = UIView()  
    setupTableViewBackgroundView()  
    setupSearchBar()  
}

An UIView is added as tableFooterView so that empty cells will not be visible.

Create APIFetcher

Now let us create an APIFetcher as a helper to helps us to fetch content from the API whenever we type something in the search bar with two methods inside it.


func search(searchText: String, 
            completionHandler: @escaping ([JSON]?, NetworkError) -> ()) {}
func fetchImage(url: String, 
                completionHandler: @escaping (UIImage?, NetworkError) -> ()) {}

The first method is used to hit the API with desired search text from the searchBar, the Second method is used to fetch an image from the URL received as a search result, the entire helper class will look like below:


import Foundation
import SwiftyJSON
import Alamofire

enum NetworkError: Error {
    case failure
    case success
}

class APIRequestFetcher {
    var searchResults = [JSON]()
    
    func search(searchText: String, completionHandler: @escaping ([JSON]?, NetworkError) -> ()) {
        let urlToSearch = "https://en.wikipedia.org//w/api.php?action=query&format=json&prop=pageimages%7Cpageterms&generator=prefixsearch&redirects=1&formatversion=2&piprop=thumbnail&pithumbsize=50&pilimit=10&wbptterms=description&gpssearch=\(searchText)&gpslimit=10"
        
        Alamofire.request(urlToSearch).responseJSON { response in
            guard let data = response.data else {
                completionHandler(nil, .failure)
                return
            }
            
            let json = try? JSON(data: data)
            let results = json?["query"]["pages"].arrayValue
            guard let empty = results?.isEmpty, !empty else {
                completionHandler(nil, .failure)
                return
            }
            
            completionHandler(results, .success)
        }
    }
    
    func fetchImage(url: String, completionHandler: @escaping (UIImage?, NetworkError) -> ()) {
        Alamofire.request(url).responseData { responseData in
            
            guard let imageData = responseData.data else {
                completionHandler(nil, .failure)
                return
            }
            
            guard let image = UIImage(data: imageData) else {
                completionHandler(nil, .failure)
                return
            }
            
            completionHandler(image, .success)
        }
    }
}

I have created an enumeration to pass the network status in the completion handler.

The API used here to hit and get search results is Wikipedia Media API

Now let us make the controller ready for making this API to hit and show the results,


import UIKit
import SwiftyJSON
import Alamofire
import SafariServices

final class SearchResultsTableViewController: UITableViewController {
    
    private var searchResults = [JSON]() {
        didSet {
            tableView.reloadData()
        }
    }
    
    private let searchController = UISearchController(searchResultsController: nil)
    private let apiFetcher = APIRequestFetcher()
    private var previousRun = Date()
    private let minInterval = 0.05

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.tableFooterView = UIView()
        setupTableViewBackgroundView()
        setupSearchBar()
    }
    
    private func setupTableViewBackgroundView() {
        let backgroundViewLabel = UILabel(frame: .zero)
        backgroundViewLabel.textColor = .darkGray
        backgroundViewLabel.numberOfLines = 0
        backgroundViewLabel.text = " Oops, No results to show "
        backgroundViewLabel.textAlignment = NSTextAlignment.center
        backgroundViewLabel.font.withSize(20)
        tableView.backgroundView = backgroundViewLabel
    }

    private func setupSearchBar() {
        searchController.searchBar.delegate = self
        searchController.dimsBackgroundDuringPresentation = false
        searchController.hidesNavigationBarDuringPresentation = false
        searchController.searchBar.placeholder = "Search any Topic"
        definesPresentationContext = true
        tableView.tableHeaderView = searchController.searchBar
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return searchResults.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell",
                                                 for: indexPath) as! CustomTableViewCell
        
        cell.titleLabel.text = searchResults[indexPath.row]["title"].stringValue
        
        cell.descriptionLabel.text = searchResults[indexPath.row]["terms"]["description"][0].string
        
        if let url = searchResults[indexPath.row]["thumbnail"]["source"].string {
            apiFetcher.fetchImage(url: url, completionHandler: { image, _ in
                cell.wikiImageView.image = image
            })
        }
        
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        let title = searchResults[indexPath.row]["title"].stringValue
        guard let url = URL.init(string: "https://en.wikipedia.org/wiki/\(title)")
            else { return }
        
        let safariVC = SFSafariViewController(url: url)
        present(safariVC, animated: true, completion: nil)
        tableView.deselectRow(at: indexPath, animated: true)
    }

}

extension SearchResultsTableViewController: UISearchBarDelegate {
    
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        searchResults.removeAll()
        guard let textToSearch = searchBar.text, !textToSearch.isEmpty else {
            return
        }
        
        if Date().timeIntervalSince(previousRun) > minInterval {
            previousRun = Date()
            fetchResults(for: textToSearch)
        }
    }
    
    func fetchResults(for text: String) {
        print("Text Searched: \(text)")
        apiFetcher.search(searchText: text, completionHandler: {
            [weak self] results, error in
            if case .failure = error {
                return
            }
            
            guard let results = results, !results.isEmpty else {
                return
            }
            
            self?.searchResults = results
        })
    }
    
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchResults.removeAll()
    }

}

The didSelectRow method is configured with SafariServices, the idea is whenever we tap on a cell, it will open the respective Wikipedia page in the app itself.

also, there is a delay added to hit the API after the user typed something on the searchBar to avoid multiple calls to the API unnecessarily

Every result from the API will look like:


{  
  "index": 5,  
  "ns": 0,  
  "pageid": 1389932,  
  "terms": {  
    "description": [  
      "Indian cricket player"  
    ]  
  },  
  "thumbnail": {  
    "height": 50,  
    "source": "[https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Murali_kartik_bowling.jpg/25px-Murali_kartik_bowling.jpg](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Murali_kartik_bowling.jpg/25px-Murali_kartik_bowling.jpg)",  
    "width": 25  
  },  
  "title": "Murali Kartik"  
}

among all these, we gonna map the title to our title, description to our description and thumbnail source to our imageView

That's IT, so when we run the app now, we could able to get results when we start typing in the searchBar

Demo of a simple API Search on simulator

The entire project can be downloaded here

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.