The old UISearchDisplayController class is now deprecated and instead we have to use the new UISearchController. There used to be a property in the old class called "SearchResultsTableView" but it's gone from the new class.
I populate a table with data and all works as intended - including segueing each row's details to another scene. I throw a search bar in there (programmatically - using the new searchController) and it successfully reloads the original table with any found results.
HOWEVER, when touching a selected row after a search, the segue passed along is that of the original table row that happens to be in the same position of the one touched now! (i.e. if I choose the current second row of a search, the next scene will segue the details of the second row of the original table!) That's because despite the data in the rows are being successfuly repopulated with the search data, the index numbers are still those of the old data.
It used to be with the old type that we would check this as such:
if (self.resultSearchController.active) {
let indexPath = self.searchDisplayController!.searchResultsTableView.indexPathForSelectedRow()
} else {
let indexPath = self.tableView.indexPathForSelectedRow()
So I think that with the old UISearchDisplayController class you actually got a new table, whereas with the new SearchController Class you only get new rows inside the old table? This totaly doesn't make sense !
Here is my full code per request:
import UIKit
import Foundation
class secondTableViewController: UITableViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating {
var filteredTableData = [String]()
var resultSearchController = UISearchController()
//these 2 are standard for the title and subtitle
var TableTitle:Array< String > = Array < String >()
var TableSub:Array< String > = Array < String >()
//the following are for my seque to next scene
var the_fname:Array< String > = Array < String >()
var the_basics:Array< String > = Array < String >()
var the_p_method:Array< String > = Array < String >()
var the_seats:Array< String > = Array < String >()
var the_notes:Array< String > = Array < String >()
var the_tableData:Array< String > = Array < String >()
override func viewDidLoad() {
tableView.delegate = self
tableView.dataSource = self
self.title = currentBus
super.viewDidLoad()
self.resultSearchController = ({
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.sizeToFit()
self.tableView.tableHeaderView = controller.searchBar
return controller
})()
// Reload the table
self.tableView.reloadData()
var url = "http://the_path_to_my_json_file"
get_data_from_url(url)
}
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 Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// 2
if (self.resultSearchController.active) {
return self.filteredTableData.count
}
else {
return TableTitle.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("secondtableCell", forIndexPath: indexPath) as! UITableViewCell
// Configure the cell...
if (self.resultSearchController.active) {
cell.textLabel?.text = filteredTableData[indexPath.row]
//cell.detailTextLabel?.text = TableSub[indexPath.row]
}else{
cell.textLabel?.text = TableTitle[indexPath.row]
cell.detailTextLabel?.text = TableSub[indexPath.row]
}
return cell
}
func get_data_from_url(url:String)
{
let httpMethod = "GET"
let timeout = 15
let url = NSURL(string: url)
let urlRequest = NSMutableURLRequest(URL: url!,
cachePolicy: .ReloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 15.0)
let queue = NSOperationQueue()
NSURLConnection.sendAsynchronousRequest(
urlRequest,
queue: queue,
completionHandler: {(response: NSURLResponse!,
data: NSData!,
error: NSError!) in
if data.length > 0 && error == nil{
let json = NSString(data: data, encoding: NSASCIIStringEncoding)
self.extract_json(json!)
}else if data.length == 0 && error == nil{
println("Nothing was downloaded")
} else if error != nil{
println("Error happened = \(error)")
}
}
)
}
func extract_json(data:NSString)
{
var parseError: NSError?
let jsonData:NSData = data.dataUsingEncoding(NSASCIIStringEncoding)!
let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(jsonData, options: nil, error: &parseError)
if (parseError == nil)
{
if let my_pass_list = json as? NSArray
{
for (var i = 0; i < my_pass_list.count ; i++ )
{
if let each_pass = my_pass_list[i] as? NSDictionary
{
if let fname = each_pass["fname"] as? String
{
if let lname = each_pass["lname"] as? String
{
if let numofseats = each_pass["numofseats"] as? String
{
if let showed_up = each_pass["showed_up"] as? String
{
if let res_id = each_pass["resnum"] as? String
{
if let res_notes = each_pass["res_notes"] as? String
{
if let payment_description = each_pass["payment_description"] as? String
{
// the_tableData.append(fname)
the_fname.append(fname)
the_basics.append(fname + " " + lname)
the_p_method.append(payment_description)
the_seats.append(numofseats)
the_notes.append(res_notes)
TableTitle.append(fname + " " + lname)
TableSub.append("Seats Reserved: " + numofseats + ". Showed Up: " + showed_up + ". Notes:" + res_notes)
the_tableData = TableTitle
}
}
}
}
}
}
}
}
}
}
}
do_table_refresh();
}
func do_table_refresh()
{
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
return
})
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using [segue destinationViewController].
var thirdScene = segue.destinationViewController as! customer_details_View_Controller
if let indexPath = self.tableView.indexPathForSelectedRow() {
/*
so what I'm missing is to be able to check
if (self.resultSearchController.active) {
and if yes have indexPath be the self.resultSearchController.resultSearchTableView.indexPathForSelectedRow() {
or something of that nature
*/
thirdScene.dotrav = todayString
thirdScene.from = currentBus
thirdScene.basics = the_basics[indexPath.row]
thirdScene.p_method = the_basics[indexPath.row]
thirdScene.seats = the_tableData[indexPath.row]
thirdScene.notes = the_notes[indexPath.row]
}
// Pass the selected object to the new view controller.
}
func updateSearchResultsForSearchController(searchController: UISearchController)
{
filteredTableData.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text)
let array = (the_tableData as NSArray).filteredArrayUsingPredicate(searchPredicate)
filteredTableData = array as! [String]
self.tableView.reloadData()
}
}
You need to account for the fact that you are going to have different data in your tableView depending on the search result. You can still use self.tableView.indexPathForSelectedRow.
What I do, is keep a reference to my base data, and then keep a reference to my filtered data, and display my filtered data in the tableView at all times. If my searchBar has no text, then my filtered data is equal to my base data.
Example:
class MyTableViewController: UITableViewController, UISearchResultsUpdating {
var data: [String] = ["One", "Two", "Three", "Four", "Five"]
var filteredData: [String]!
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
setUpSearchController()
setFilteredDataForCurrentSearch()
}
private func setUpSearchController() {
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
self.tableView.tableHeaderView = searchController.searchBar
}
private func setFilteredDataForCurrentSearch() {
if let searchString = searchController.searchBar.text where !searchString.isEmpty {
filteredData = data.filter({ (string: String) -> Bool in
return searchString.rangeOfString(string, options: NSStringCompareOptions.CaseInsensitiveSearch) != nil
})
} else {
filteredData = data
}
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
setFilteredDataForCurrentSearch()
}
}
Now, you can implement all of your UITableViewDataSource and UITableViewDelegate methods using the filteredData.
In prepareForSegue, you retrieve the correct selected object like:
let indexPath = tableView.indexPathForSelectedRow()
let selectedObject = filteredData[indexPath.row]
Related
I currently have NSArray that gets its data from a mySQL database.
I need to filter this data based on a hard-coded string "Customer1"
The following is what I have so far:
import UIKit
class showCustomerDetails: UIViewController, UITableViewDataSource, UITableViewDelegate, FeedDetailProtocol {
var feedItems: NSArray = NSArray()
var selectedStock : DetailModel = DetailModel()
#IBOutlet weak var stockResultsFeed: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.stockResultsFeed.delegate = self
self.stockResultsFeed.dataSource = self
let detailModel = FeedDetail()
detailModel.delegate = self
detailModel.downloadItems()
}
func itemsDownloaded(items: NSArray) {
feedItems = items
self.stockResultsFeed.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of feed items
return feedItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Retrieve cell
let cellIdentifier: String = "customerDetails"
let myCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
myCell.textLabel?.textAlignment = .center
// Get the stock to be shown
let item: DetailModel = feedItems[indexPath.row] as! DetailModel
// Configure our cell title made up of name and price
let customerDetails = [item.code, item.manufacturer, item.model].compactMap { $0 }.joined(separator: " — ")
print(customerDetails)
// Get references to labels of cell
myCell.textLabel!.text = customerDetails
return myCell
}
}
The following is what I was thinking of doing, but I am not sure how to properly apply it:
let searchString = "Customer1"
let predicate = NSPredicate(format: "SELF contains %#", searchString)
let searchDataSource = feedItems.filter { predicate.evaluateWithObject($0) }
And then:
let item: DetailModel = searchDataSource[indexPath.row] as! DetailModel
NSArray data is coming from:
import Foundation
protocol FeedDetailProtocol: class {
func itemsDownloaded(items: NSArray)
}
class FeedDetail: NSObject, URLSessionDataDelegate {
weak var delegate: FeedDetailProtocol!
let urlPath = "https://www.example.com/test1/test1.php"
func downloadItems() {
let url: URL = URL(string: urlPath)!
let defaultSession = Foundation.URLSession(configuration: URLSessionConfiguration.default)
let task = defaultSession.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Error")
}else {
print("details downloaded")
self.parseJSON(data!)
}
}
task.resume()
}
func parseJSON(_ data:Data) {
var jsonResult = NSArray()
do{
jsonResult = try JSONSerialization.jsonObject(with: data, options:JSONSerialization.ReadingOptions.allowFragments) as! NSArray
} catch let error as NSError {
print(error)
}
var jsonElement = NSDictionary()
let stocks = NSMutableArray()
for i in 0 ..< jsonResult.count
{
jsonElement = jsonResult[i] as! NSDictionary
let stock = DetailModel()
//the following insures none of the JsonElement values are nil through optional binding
if let code = jsonElement[“code”] as? String,
let customer = jsonElement["customer"] as? String,
let manufacturer = jsonElement["manufacturer"] as? String,
let model = jsonElement["model"] as? String
{
print(code)
print(manufacturer)
print(model)
print(customer)
stock.code = code
stock.manufacturer = manufacturer
stock.model = model
stock.customer = customer
}
stocks.add(stock)
}
DispatchQueue.main.async(execute: { () -> Void in
self.delegate.itemsDownloaded(items: stocks)
})
}
}
This is Swift. Use Array, not NSArray, and just call Array's filter method. NSArray belongs to Cocoa and Objective-C; you should use native Swift types and Swift methods as much as possible.
If you insist on filtering an NSArray using a Cocoa Objective-C method, and you insist on using NSPredicate, the simplest approach is to form your predicate with init(block:).
Here's a simple illustration:
let arr = ["Manny", "Moe", "Jack"] as NSArray
let p = NSPredicate { element, _ in
return (element as? String)?.contains("a") ?? false
}
let arr2 = arr.filtered(using: p)
print(arr2) // [Manny, Jack]
But (just to drive home the point) it's so much simpler in native Swift:
let arr = ["Manny", "Moe", "Jack"]
let arr2 = arr.filter {$0.contains("a")}
Hi I have two arrays and only one array is updating with search bar.. I keep the TitleArray to show in tableView title and detailsArray to show in tableView subtitle.. once I start searching only title following my typing but subtitle nothing change.
#IBOutlet weak var AirportsTableView: UITableView!
var TitleArray = [String]()
var DetailsArray = [String]()
var NumberOfRows = 0
var filteredNamesArray = [String]()
var filteredDetailsArray = [String]()
var resultSearchController = UISearchController!()
**override func viewDidLoad() {
super.viewDidLoad()**
// Do any additional setup after loading the view.
self.resultSearchController = UISearchController(searchResultsController: nil)
self.resultSearchController.searchResultsUpdater = self
self.resultSearchController.dimsBackgroundDuringPresentation = false
self.resultSearchController.searchBar.sizeToFit()
self.resultSearchController.loadViewIfNeeded()
self.AirportsTableView.tableHeaderView = self.resultSearchController.searchBar
self.AirportsTableView.reloadData()
parseJSON()
}
func parseJSON() {
if let path = NSBundle.mainBundle().pathForResource("airports", ofType: "json") {
do {
let data = try NSData(contentsOfURL: NSURL(fileURLWithPath: path), options: NSDataReadingOptions.DataReadingMappedIfSafe)
let jsonObj = JSON(data: data)
if jsonObj != JSON.null {
// print("jsonData:\(jsonObj)")
NumberOfRows = jsonObj.count
for i in 0...NumberOfRows {
let City = jsonObj[i]["city"].string as String!
let Country = jsonObj[i]["country"].string as String!
let Iata = jsonObj[i]["iata"].string as String!
let Name = jsonObj[i]["name"].string as String!
self.TitleArray.append("\(City) - \(Country) - \(Iata)")
self.DetailsArray.append("\(Name)")
}
} else {
print("could not get json from file, make sure that file contains valid json.")
}
} catch let error as NSError {
print(error.localizedDescription)
}
} else {
print("Invalid filename/path.")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
// MARK: - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
if self.resultSearchController.active
{
return self.filteredNamesArray.count
} else
{
return self.TitleArray.count
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell?
if self.resultSearchController.active
{
cell!.textLabel?.text = self.filteredNamesArray[indexPath.row]
} else
{
cell!.textLabel?.text = self.TitleArray[indexPath.row]
cell!.detailTextLabel?.text = self.DetailsArray[indexPath.row]
}
return cell!
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.filteredNamesArray.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text!)
let array = (self.TitleArray as NSArray).filteredArrayUsingPredicate(searchPredicate)
self.filteredNamesArray = array as! [String]
self.AirportsTableView.reloadData()
}
// MARK: - Segues
/*
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "AirportDetails" {
if let indexPath = self.AirportsTableView.indexPathForSelectedRow {
let airportDetail : Airports = TitleArray[indexPath.row]
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! AllWaysFlightsViewController
controller.airportDetail = airportDetail
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
*/
Instead of using two separate arrays use only one array and populate it with object containing both variables you are using to populate the tableView.
class Address {
var city: String
var detail: String
init(city: String, detail:String) {
self.city = city
self.detail = detail
}
}
Parse your json like this:
for i in 0...NumberOfRows {
let City = jsonObj[i]["city"].string as String!
let Country = jsonObj[i]["country"].string as String!
let Iata = jsonObj[i]["iata"].string as String!
let Name = jsonObj[i]["name"].string as String!
let city = "\(City) - \(Country) - \(Iata)"
let address = Address(city: city, detail: Name)
self.TitleArray.append(address)
self.filteredNamesArray.append(address)
}
Filter your title array containing addresses. Your titlearray and filtered array both contains same data for the first time you can refer to the json parsing for this. Here you can use one for filtering and when search bar is empty it user cancel his search you can re-populate your array from the other one.
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.filteredNamesArray.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF.city CONTAINS[c] %#", searchController.searchBar.text!)
let array = (self.TitleArray as NSArray).filteredArrayUsingPredicate(searchPredicate)
self.filteredNamesArray = array as! [Address]
self.AirportsTableView.reloadData()
}
your tableView logic will be changed accordingly
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.filteredNamesArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell?
let address = self.filteredNamesArray[indexPath.row]
cell!.textLabel?.text = address?.city
cell!.detailTextLabel?.text = address?.detail
return cell!
}
You need to change the way you approach filtering the data so that rather than just apply a predicate you explicitly iterate and check the predicate, if you find a match then you take that item and the corresponding description into your filtered arrays.
Something like:
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.filteredNamesArray.removeAll(keepCapacity: false)
self.filteredDetailsArray.removeAll(keepCapacity: false)
let searchString = searchController.searchBar.text!
var index = 0
for title in self.TitleArray
if title.rangeOfString(searchString).location != NSNotFound {
self.filteredNamesArray.append(title)
self.filteredDetailsArray.append(self.DetailsArray[index])
}
index += 1
}
self.AirportsTableView.reloadData()
}
I am attempting to send an array of data to a new view controller and I'm currently getting the error fatal error: unexpectedly found nil while unwrapping an Optional value
Im using the API data from www.thecocktaildb.com
Example:
http://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita
Not sure what I'm doing wrong. Tried debugging and checking values before the segue in my search view controller and they're accurate.
Heres my code:
Main Storyboard
SearchViewController
class SearchViewController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var TableView: UITableView!
#IBOutlet weak var SearchBar: UISearchBar!
var valueToPass:Drinks!
var isSearching : Bool = false
class Drinks {
var idDrink: Int = 0
var strDrink: String = ""
var strCategory: String = ""
var strAlcoholic: String = ""
var strGlass: String = ""
var strInstructions: String = ""
var strDrinkThumb: String = ""
var strIngredient1: String = ""
var strIngredient2: String = ""
var strIngredient3: String = ""
var strIngredient4: String = ""
var strIngredient5: String = ""
var strIngredient6: String = ""
var strIngredient7: String = ""
var strIngredient8: String = ""
var strIngredient9: String = ""
var strIngredient10: String = ""
var strIngredient11: String = ""
var strIngredient12: String = ""
var strIngredient13: String = ""
var strIngredient14: String = ""
var strIngredient15: String = ""
var strMeasure1: String = ""
var strMeasure2: String = ""
var strMeasure3: String = ""
var strMeasure4: String = ""
var strMeasure5: String = ""
var strMeasure6: String = ""
var strMeasure7: String = ""
var strMeasure8: String = ""
var strMeasure9: String = ""
var strMeasure10: String = ""
var strMeasure11: String = ""
var strMeasure12: String = ""
var strMeasure13: String = ""
var strMeasure14: String = ""
var strMeasure15: String = ""
}
var TableData:Array< Drinks > = Array < Drinks >()
override func viewDidLoad() {
super.viewDidLoad()
for subView in self.SearchBar.subviews
{
for subsubView in subView.subviews
{
if let textField = subsubView as? UITextField
{
textField.attributedPlaceholder = NSAttributedString(string: NSLocalizedString("Search", comment: ""))
}
}
}
self.SearchBar.delegate = self
self.TableView.delegate = self
self.TableView.dataSource = self
}
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
if self.SearchBar.text!.isEmpty {
self.isSearching = false
}else{
self.isSearching = true
let userSearchInput = self.SearchBar.text!.lowercaseString
let newString = userSearchInput.stringByReplacingOccurrencesOfString(" ", withString: "%20", options: NSStringCompareOptions.LiteralSearch, range: nil)
let postEndpoint: String = "http://www.thecocktaildb.com/api/json/v1/1/search.php?s=" + newString
guard let url = NSURL(string: postEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = NSURLRequest(URL: url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) in
guard let responseData = data else {
print("Error: did not receive data")
return
}
guard error == nil else {
print("error calling GET on www.thecocktaildb.com")
print(error)
return
}
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
print("error trying to convert data to JSON")
return
}
var count = 1
if let drinks = post["drinks"] as? [NSDictionary] {
self.TableData.removeAll()
for drink in drinks {
let adrink = Drinks()
if let strDrink = drink["strDrink"] as? String {
print(String(count) + ". " + strDrink)
adrink.strDrink = strDrink
count++
}
if let strCategory = drink["strCategory"] as? String {
print(" Category: " + strCategory)
adrink.strCategory = strCategory
}
if let strDrinkThumb = drink["strDrinkThumb"] as? String {
print(" Thumbnail Image: " + strDrinkThumb)
adrink.strDrinkThumb = strDrinkThumb
}
self.TableData.append(adrink)
self.TableView.reloadData()
}
}
})
task.resume()
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return TableData.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
//title = TableData[indexPath.row].strDrink
cell.textLabel?.text = TableData[indexPath.row].strDrink;
let imageString = TableData[indexPath.row].strDrinkThumb
if (imageString == ""){
let noDrinkImage : UIImage = UIImage(named: "noimage.jpg")!
cell.imageView!.image = noDrinkImage
}else{
let drinkImage = UIImage(data: NSData(contentsOfURL: NSURL(string:TableData[indexPath.row].strDrinkThumb)!)!)
cell.imageView!.image = drinkImage
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
print(TableData[indexPath.row].strDrink)
valueToPass = TableData[indexPath.row]
//self.performSegueWithIdentifier("drinkSegue", sender: TableData[indexPath.row])
}
// hide kwyboard when search button clicked
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
self.SearchBar.resignFirstResponder()
}
// hide keyboard when cancel button clicked
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
self.SearchBar.text = ""
self.SearchBar.resignFirstResponder()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "DrinkSegue") {
// initialize new view controller and cast it as your view controller
let drinkViewController = segue.destinationViewController as! DrinkViewController
// your new view controller should have property that will store passed value
drinkViewController.passedValue = valueToPass
}
}
}
DrinkViewController.swift
class DrinkViewController: UIViewController {
#IBOutlet weak var DrinkNameLabel: UILabel!
var passedValue : SearchViewController.Drinks!
override func viewDidLoad() {
super.viewDidLoad()
DrinkNameLabel.text = passedValue!.strDrink
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Do it like this instead
In your didSelectRowAtIndexPath pass the array
self.performSegueWithIdentifier("drinkSegue", sender: TableData[indexPath.row])
Here you need to pass the array to your DrinkViewController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "DrinkSegue") {
// initialize new view controller and cast it as your view controller
let drinkViewController = segue.destinationViewController as! DrinkViewController
// your new view controller should have property that will store passed value
drinkViewController.passedValue = valueToPass
// declare myArray in your drinkViewController and then assign it here
// now your array that you passed will be available through myArray
drinkViewController.myArray = sender
}
}
Update
After I got your project I noticed that the issue you had was that you did drag a segue from the tableView to the drinksController directly - what happened is that didSelectRowAtIndexPath will not be called and your sender will always be nil drinkViewController.myArray = sender as! Drinks.
I changed that by dragging the segue from the viewController to the drinksController instead.
I am loading a UITableViewController with data from my Contact List. Everything works fine in simulator with the small set of data, but on my iPhone with 82 contacts, i get a varying number of rows loaded, from 0, to most of the data, but never all of the data, and always a different amount. It's almost as though the table is being loaded and displayed before the data array is complete. If i set the numberOfRowsInSection return manually to 82 it works fine, but when set to contactdatalist.count it gets a lesser number. Is there something i am doing wrong, or can i slow down the tableview load until all the data load is complete? It seems asynchronous in operation.
import UIKit
import AddressBook
class TableViewControllerContacts: UITableViewController {
var contactdatalist = [ContactData]()
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()
self.contactdatalist = []
let addressBook : ABAddressBookRef? = ABAddressBookCreateWithOptions(nil, nil).takeRetainedValue()
ABAddressBookRequestAccessWithCompletion(addressBook, { (granted : Bool, error: CFError!) -> Void in
if granted == true {
let allContacts : NSArray = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue()
for contactRef:ABRecordRef in allContacts { // first name
let myPBfirstname = ABRecordCopyValue(contactRef, kABPersonFirstNameProperty)?.takeRetainedValue() as! NSString? ?? ""
let myPBlastname = ABRecordCopyValue(contactRef, kABPersonLastNameProperty)?.takeRetainedValue() as! NSString? ?? ""
let phonesRef: ABMultiValueRef = ABRecordCopyValue(contactRef, kABPersonPhoneProperty)?.takeRetainedValue() as ABMultiValueRef? ?? ""
var phonesArray = Array<Dictionary<String,String>>()
for var i:Int = 0; i < ABMultiValueGetCount(phonesRef); i++ {
let myPhLabel = ABMultiValueCopyLabelAtIndex(phonesRef, i)?.takeRetainedValue() as NSString? ?? ""
let myPhValue = ABMultiValueCopyValueAtIndex(phonesRef, i)?.takeRetainedValue() as! NSString? ?? ""
if myPhLabel.containsString("Mobile") {
self.contactdatalist.append(ContactData(firstname:myPBfirstname as String, lastname:myPBlastname as String, phone:myPhValue as String))
}
}
}
}
})
self.tableView.reloadData()
}
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 Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
return self.contactdatalist.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
// Configure the cell...
let contactdata = self.contactdatalist[indexPath.row]
cell.textLabel!.text = ("\(contactdata.firstname) \(contactdata.lastname)")
return cell
}
Update your viewDidLoad, so that contacts are fetched in a separate thread, and tableview is loaded when the contacts are fetched.
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()
self.contactdatalist = []
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) {
let addressBook : ABAddressBookRef? = ABAddressBookCreateWithOptions(nil, nil).takeRetainedValue()
ABAddressBookRequestAccessWithCompletion(addressBook, { (granted : Bool, error: CFError!) -> Void in
if granted == true {
let allContacts : NSArray = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue()
for contactRef:ABRecordRef in allContacts { // first name
let myPBfirstname = ABRecordCopyValue(contactRef, kABPersonFirstNameProperty)?.takeRetainedValue() as! NSString? ?? ""
let myPBlastname = ABRecordCopyValue(contactRef, kABPersonLastNameProperty)?.takeRetainedValue() as! NSString? ?? ""
let phonesRef: ABMultiValueRef = ABRecordCopyValue(contactRef, kABPersonPhoneProperty)?.takeRetainedValue() as ABMultiValueRef? ?? ""
var phonesArray = Array<Dictionary<String,String>>()
for var i:Int = 0; i < ABMultiValueGetCount(phonesRef); i++ {
let myPhLabel = ABMultiValueCopyLabelAtIndex(phonesRef, i)?.takeRetainedValue() as NSString? ?? ""
let myPhValue = ABMultiValueCopyValueAtIndex(phonesRef, i)?.takeRetainedValue() as! NSString? ?? ""
if myPhLabel.containsString("Mobile") {
self.contactdatalist.append(ContactData(firstname:myPBfirstname as String, lastname:myPBlastname as String, phone:myPhValue as String))
}
}
}
}
})
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
Requesting the address book is an asynchronous operation. So you should reload the data in the table view inside that block at last step, and not at the end of viewDidLoad.
I am learning iOS swift and creating an application to learn about getting JSON data and saving this data to CoreData while working with Itunes search api. I have a table view and am using a custom table view cell, it has some labels, an image and a download button. My purpose is to able to get album and all songs in that album information to CoreData after clicking the button of the cell. Here is the list of what is working and what is not working:
Clicking the button gives me the correct CollectionId for the album.
The album information is successfully added to CoreData.
I'm NOT able to fill my songs array after calling the api in my download action method. It stays empty. Note that when I call the api in ViewDidLoad with a manually entered collection id, the songs array is filled.
Codes:
API Controller to get the song information.
import Foundation
protocol songAPIControllerForCoreDataProtocol {
func didReceiveAPISongResults(results: NSDictionary)
}
class songAPIControllerForCoreData {
var delegate: songAPIControllerForCoreDataProtocol
init(delegate: songAPIControllerForCoreDataProtocol) {
self.delegate = delegate
}
func searchItunesForSongsBelongingTo(searchTerm: String) {
// The iTunes API wants multiple terms separated by + symbols, so I'm replacing spaces with + signs
let itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
// Escape anything else that isn't URL-friendly
if let escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) {
// Using Itunes search api to find people that has a music album with the entered search term
let urlPath = "https://itunes.apple.com/lookup?id=\(escapedSearchTerm)&entity=song"
let url: NSURL = NSURL(string: urlPath)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if(error != nil) {
// If there is an error in the web request, print it to the console
println(error.localizedDescription)
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as! NSDictionary
println(jsonResult[0])
if(err != nil) {
// If there is an error parsing JSON, print it to the console
println("JSON Error \(err!.localizedDescription)")
}
self.delegate.didReceiveAPISongResults(jsonResult)
println(jsonResult)
})
task.resume()
}
}
}
Song class (Not CoreData):
import Foundation
class Song {
var title: String
var previewURL: String
var collectionID: Int
init(title: String, previewURL: String, collectionID: Int) {
self.title = title
self.previewURL = previewURL
self.collectionID = collectionID
}
class func songsWithJSON(allResults: NSArray) -> [Song] {
// Create an empty array of Albums to append to from this list
var songs = [Song]()
// Store the results in our table data array
if allResults.count>0 {
// Sometimes iTunes returns a collection, not a track, so we check both for the 'name'
for result in allResults {
var title = result["trackName"] as? String
if title == nil {
title = result["collectionName"] as? String
}
if title == nil {
title = result["collectionName"] as? String
}
let previewURL = result["previewUrl"] as? String ?? ""
let collectionID = result["collectionId"] as? Int ?? 0
var newSong = Song(title: title!, previewURL: previewURL, collectionID: collectionID)
songs.append(newSong)
}
}
return songs
}
}
Finally AlbumViewController:
import UIKit
import CoreData
class AlbumViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, searchAPIControllerProtocol, songAPIControllerForCoreDataProtocol {
#IBOutlet
var tableView: UITableView!
#IBOutlet weak var artistNameOutlet: UILabel!
var songapi : songAPIControllerForCoreData?
var api : searchAPIController?
var albums = [Album]()
var songs = [Song]()
var imageCache = [String : UIImage]()
//Variables that take the values after segue from uTableViewController
var artistID, artistName: String?
let cellIdentifier: String = "albumCell"
//for CoreData
var error:NSError?
let managedObjectContext = (UIApplication.sharedApplication().delegate
as! AppDelegate).managedObjectContext
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.albums.count
}
func download(sender: AnyObject){
var senderButton : UIButton = sender as! UIButton
let newAlbum = NSEntityDescription.insertNewObjectForEntityForName("Albums", inManagedObjectContext: managedObjectContext!) as! Albums
let newSong = NSEntityDescription.insertNewObjectForEntityForName("Songs", inManagedObjectContext: managedObjectContext!) as! Songs
songapi!.searchItunesForSongsBelongingTo((String)(self.albums[senderButton.tag].collectionID))
newAlbum.albumArt = self.albums[senderButton.tag].largeImageURL
newAlbum.albumID = (String)(self.albums[senderButton.tag].collectionID)
newAlbum.albumName = self.albums[senderButton.tag].title
newAlbum.albumPrice = self.albums[senderButton.tag].price
newAlbum.artistID = self.artistID!
newAlbum.artistName = self.artistName!
newAlbum.numberOfSongs = (String)(self.albums[senderButton.tag].trackCount)
newAlbum.has = []
println(self.songs)
for(var i = 1; i < self.albums[senderButton.tag].trackCount - 1; i++){
newSong.collectionID = String(self.songs[i].collectionID)
newSong.previewURL = self.songs[i].previewURL
newSong.songName = self.songs[i].title
}
self.managedObjectContext?.save(&self.error)
println(newAlbum)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: AlbumTableViewCell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! AlbumTableViewCell
cell.albumCellButton.tag = indexPath.row
cell.albumCellButton.addTarget(self, action: "download:", forControlEvents: .TouchUpInside)
let album = self.albums[indexPath.row]
cell.albumName.text = album.title
cell.artistImage.image = UIImage(named: "user7.png")
cell.numberOfSongs.text = (String)(album.trackCount) + " Songs"
// Get the formatted price string for display in the subtitle
let formattedPrice = album.price
// Grab the artworkUrl60 key to get an image URL for the app's thumbnail
let urlString = album.thumbnailImageURL
// Check our image cache for the existing key. This is just a dictionary of UIImages
var image = self.imageCache[urlString]
if( image == nil ) {
// If the image does not exist, we need to download it
var imgURL: NSURL = NSURL(string: urlString)!
// Download an NSData representation of the image at the URL
let request: NSURLRequest = NSURLRequest(URL: imgURL)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
if error == nil {
image = UIImage(data: data)
// Store the image in to our cache
self.imageCache[urlString] = image
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) as?AlbumTableViewCell {
cellToUpdate.artistImage.image = image
}
})
}
else {
println("Error: \(error.localizedDescription)")
}
})
}
else {
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) as?AlbumTableViewCell {
cellToUpdate.artistImage.image = image
}
})
}
cell.priceOfAlbum.text = formattedPrice
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
}
func didReceiveAPIResults(results: NSDictionary) {
var resultsArr: NSArray = results["results"] as! NSArray
dispatch_async(dispatch_get_main_queue(), {
self.albums = Album.albumsWithJSON(resultsArr)
self.tableView!.reloadData()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
})
}
func didReceiveAPISongResults(results: NSDictionary) {
var resultsArr: NSArray = results["results"] as! NSArray
dispatch_async(dispatch_get_main_queue(), {
self.songs = Song.songsWithJSON(resultsArr)
self.tableView!.reloadData()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
})
}
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = artistName
artistNameOutlet.text = " Albums"
api = searchAPIController(delegate: self)
songapi = songAPIControllerForCoreData(delegate: self)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
api!.searchItunesForAlbumsBelongingTo(self.artistName!, id: self.artistID!)
// Do any additional setup after loading the view.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let songsController = segue.destinationViewController as! SongsViewController
var albumCollectionID = self.albums
var albumIndex = tableView!.indexPathForSelectedRow()!.row
var collectionID = self.albums[albumIndex].collectionID
var albumName = self.albums[albumIndex].title
songsController.albumName = albumName
songsController.collectionID = collectionID
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You need to write the definition of your protocol like follows:
protocol songAPIControllerForCoreDataProtocol : class {
func didReceiveAPISongResults(results: NSDictionary)
}
This will make it class only protocol and will force the confirming type to have reference semantics. If no 'class' keyword is specified it will have value semantics.
Without the 'class' keyword the issue here I assume is setting the delegate via initializer. When you pass delegate like:
songapi = songAPIControllerForCoreData(delegate: self)
This will assume the delegate param to be on value type and copy the value rather than send a reference of it. So when you set that value in init() the delegate member will point to a new object rather than the UIViewController passed.
If you set the delegate like:
songapi.delegate = self
it will work without the 'class' keyword in protocol definition.