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.
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.