Search code examples
iosswiftuitableviewuisearchdisplaycontroller

Trouble Populating UISearchBarController Results TableView


I'm new to Swift, so be gentle.

I'm using the Xcode 7 Beta, currently.

I have a TableViewController nested in a NavigationController. Within the TableView I've implemented a UISearchBar and UISearchDisplayController similar to the steps here. Pretty much everything is working as expected, except when I type my search criteria, the results table view for the UISearchDisplayController is not getting populated. When I hit Cancel on the search, the results have been populated in the initial table view.

The tutorial I linked to is pre-populating a list of items and the search bar is filtering these items. My approach is different because I'm populating the search from an external API.

My question is two-fold:

a) how do I properly populate the results TableView?

b) the two TableViews seem redundant and I don't feel the first is necessary (might be wrong). What's the proper way to achieve this functionality? Ideally, I would like to be able to put a UISearchBar in the navigation item so it lives in the UINavigationBar, but I have had trouble finding resources for doing that.

import UIKit
import Alamofire
import SwiftyJSON
import Foundation

class DSTableViewController: UITableViewController, UISearchBarDelegate, UISearchDisplayDelegate {

  var songs: [Song] = []
  var timer: NSTimer = NSTimer()

  func getSongs(timer: NSTimer!) {

    let searchTerm = timer.userInfo as! String

    self.title = "Results for \(searchTerm)"

    // logic to switch service; need to eventually move this to an interface
    // or move this code inside a rest API, and then into an interface

    // URL encode the search term
    let searchTermEncoded = searchTerm.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())

    // create the url for the web request
    let uri: String = "https://api.spotify.com/v1/search?q=\(searchTermEncoded!)&type=artist,album,track"

    // call to Spotify API
    Alamofire
        .request(.GET, uri)
        .response { request, response, data, error in

            let json = JSON(data: data!)

            print(json["tracks"]["items"].count);
            print(json["tracks"]["items"])

            for var i = 0; i < json["tracks"]["items"].count; i++ {

                let data = json["tracks"]["items"][i]

                // return the object list
                let song = Song()

                song.title = data["name"].string!
                song.album = data["album"]["name"].string!
                song.artist = data["artists"][0]["name"].string!

                self.songs += [song]

            }

            dispatch_async(dispatch_get_main_queue()) {
                self.tableView!.reloadData()
            }
      }

  }

  override func viewDidLoad() {
    super.viewDidLoad()

    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem()
  }

  func searchDisplayController(controller: UISearchDisplayController, shouldReloadTableForSearchString searchString: String?) -> Bool {

    timer.invalidate()

    timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("getSongs:"), userInfo: searchString, repeats: false)

    return true

  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }

  // MARK: - Table view data source

  override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
  }

  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows
    return songs.count
  }


  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = self.tableView.dequeueReusableCellWithIdentifier("SongCell", forIndexPath: indexPath)

    let song = songs[indexPath.row]

    cell.textLabel?.text = song.title
    cell.detailTextLabel?.text = song.artist + " - " + song.album
    cell.imageView?.image = song.albumImage

    return cell
  }
}

When I call

func searchDisplayController(controller: UISearchDisplayController, shouldReloadTableForSearchString searchString: String?) -> Bool {

    self.getSongs(searchString)

    return true

  }

I'm able to populate the view correctly, but I'd like to have the delay so I'm not making an API call every time the text changes.

Hopefully I explained that correctly. Please feel free to edit the question if I wasn't clear or missed something.


Solution

  • So I ended up getting the functionality I wanted and ended up answering my question of what the proper approach was. Turns out that I was trying to fit a square peg in a round hole.

    What I ended up doing is removing the UISearchDisplayController altogether and instead just creating a UISearchBar and assigning it to my self.navigationItem.titleView. This removed some of the built-in functionality of the search controller, but also gave me a more concise way of doing what I needed to do, without worrying about silly workarounds. My code looks roughly like

    class TableViewController: UISearchBarDelegate {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let searchBar: UISearchBar = UISearchBar()
    
            searchBar.placeholder = "Search"
            searchBar.delegate = self
    
            self.navigationItem.titleView = searchBar
    
            self.definesPresentationContext = true
        }
    
    
        func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
            timer.invalidate()
    
            timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "getResults:", userInfo: searchText, repeats: false)
        }
    
        func searchBarSearchButtonClicked(searchBar: UISearchBar) {
            searchBar.resignFirstResponder()
        }
    
    }
    

    This approach got me the functionality I was looking for (being able to delay the call to the API until the user was done typing) and I was also able to remove some code that was extraneous. Hopefully this helps someone in the future.