I've been trying to implement a search function onto this iOS app for quite some time.
I have a JSON file holding all the data, which is then pulled and displayed onto the table view controller.
I've implemented the search bar and followed several tutorials, this one being with the fewest issues - http://www.ioscreator.com/tutorials/add-search-table-view-tutorial-ios8-swift (I've also used this too previously - https://www.youtube.com/watch?v=N9wcKc37ZXI)
My JSON file is structured as follows:
{ "animal":[
{
"no":"001",
"type":"dog",
"breed":"pitbull",
"classification":"mammal",
"sprite":"beast"
},
{
"no":"002",
"type":"dog",
"breed":"bulldog",
"classification":"mammal",
"sprite":"beast"
},
{
"no":"003",
"type":"cat",
"breed":"birman",
"classification":"mammal",
"sprite":"feline"
}
]}
I am calling the JSON data using a struct that is created in a separate file
Animal.swift
struct animalStruct {
static let path = NSBundle.mainBundle().pathForResource("animal", ofType: "JSON")
static let jsonData = NSData(contentsOfFile:path!, options: .DataReadingMappedIfSafe, error: nil)
static var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(jsonData!, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
}
And it is called into the table view controller using the following
AnimalTableViewController
var array : NSArray = Animal.animalStruct.jsonResult["animal"] as NSArray
Code used for the search:
AnimalTableViewController
#IBOutlet var segmentedSortOption: UISegmentedControl!
var array : NSArray = Animal.pokemonStruct.jsonResult["animal"] as NSArray
var filteredAnimal = [String]()
var resultSearchController = UISearchController()
override func viewDidLoad() {
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
})()
self.tableView.reloadData()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == self.searchDisplayController?.searchResultsTableView {
return self.filteredAnimal.count
} else {
return self.array.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var myCell:cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as cell
var upperCasedNames = array[indexPath.row]["name"] as? String
var searchedItem:String
if segmentedSortOption.selectedSegmentIndex == 0 {
if (self.resultSearchController.active) {
//**ISSUE OCCURS HERE**
myCell.name.text = filteredAnimal[indexPath.row]
return myCell
}
else {
myCell.name.text = upperCasedNames?.uppercaseString
return myCell
}
} else if segmentedSortOption.selectedSegmentIndex == 1 {
if let unsortedEvents = Animal.animalStruct.jsonResult["animal"] as? NSArray {
let descriptor = NSSortDescriptor(key: "name", ascending: true, selector: "caseInsensitiveCompare:")
let aToZ = unsortedEvents.sortedArrayUsingDescriptors([descriptor])
upperCasedNames = aToZ[indexPath.row]["name"] as? String
myCell.name.text = upperCasedNames?.uppercaseString
}
}
return myCell
}
func updateSearchResultsForSearchController(searchController: UISearchController)
{
filteredAnimal.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text)
let arrays = (array).filteredArrayUsingPredicate(searchPredicate!)
filteredArray = arrays as [[String]]
self.tableView.reloadData()
}
}
My question
The issue I have is that when I attempt to search via the UISearchBar, the moment I click on it, I get the "fatal error: array cannot be bridged from Objective-C". I believe this is happening because the arrays used in the tutorial is a different type compared to the ones I have used.
UPDATE
Firstly, the main issue was that the fact I had a different type of an array compared to the tutorial; so I had changed the var filteredAnimal = [String]() to var filteredArray = [[String:AnyObject]]() since the data source/JSON file had numerous arrays of data. And treat it as AnyObject due to that, which then allowed me to call the specific information from the JSON file.
The reason why I was returned with an empty array was because I did not assign the searched values in the empty array within the updateSearchResultsController method, so I had added the following just before reloading the table data - filteredArray = arrays as [[String:AnyObject]]
This still did not solve the issue as I had to change the if statement within the numberOfRowsInSection from tableView == self.searchDisplayController?.searchResultsTableView to self.resultSearchController.active, which then retrieved me an actual result.
However the search itself is still not working, but majority of the issues have been resolved. When tapping the search bar, it only retrieves the final index of the array, and when searching anything, it removes the value. I will continue to work on it and hopefully resolve the matter soon. Thanks for the help so far!
Below link may help you to solve your issue:
How can I fix "fatal error: can't index empty buffer"
in your code
let arrays = (array).filteredArrayUsingPredicate(searchPredicate!)
will create new reference of arrays.
Since the last update (which has played a major part for the solution) all I did was change the NSPredicate query from SELF CONTAINS[c] %# to name CONTAINS[c] %# and now when searching, it displays the correct matching object based on its name.
Related
I have an application that loads list of questions from JSON data and shows them on TableView.
Everything is working fine most of the time but it seems to be that I am doing something wrong and that is why - app crashes.
It happens rarely so it is hard to detect but I am sure that there must a problem with the logic.
So I have model class for question and array for question items :
class questionItem {
var id = 0
var title : String = ""
var question : String = ""
}
var questions = [questionItem]()
Inside my ViewController I have IBOutlet for TableView and I placed data loading inside viewDidLoad
class QuestionsListVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var questionsTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
get_questions()
}
func get_questions()
{
let request = NSMutableURLRequest(URL:myURL!)
request.HTTPMethod = "POST"
let postString = ""
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{
data, response, error in
if error != nil {
print("error=\(error)")
return
}
//clearing array for new items
questions.removeAll(keepCapacity: false)
dispatch_async(dispatch_get_main_queue(),{
var json = JSON(data: data!)
if let items = json["questions"].array {
for item in items {
let question = questionItem()
question.id = item["id"].int!
question.title = item["title"].string!;
question.question = item["question"].string!
questions.append(question)
}
}
self.questionsTableView.reloadData()
});
}
task.resume()
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return questions.count
}
Error is shown inside cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : QuestionsListCell = self.questionsTableView.dequeueReusableCellWithIdentifier("QuestionsListCell") as! QuestionsListCell
//error happens here - Index out of range
print(questions[indexPath.row].title)
It happens one time in six cases and there is no errors in other 5 of 6 tests - but I don't understand why.
This points to a problem with the
numberOfSectionsInTableView
and/or
numberOfRowsInSection
Can you post your current implementation of these?
If you only displaying one continuous list, the numberOfSectionsInTableView should always return 1, and you need to check numberOfRowsInSection is accurately returning the number of items in the datasource.
Edit:
Can you try clearing the existing datasource on the main thread immediately before updating with the new items as in the code below:
dispatch_async(dispatch_get_main_queue(),{
questions.removeAll(keepCapacity: false)
var json = JSON(data: data!)
if let items = json["questions"].array {
for item in items {
let question = questionItem()
question.id = item["id"].int!
question.title = item["title"].string!;
question.question = item["question"].string!
questions.append(question)
}
}
self.questionsTableView.reloadData()
});
The call to questions.removeAll in your code makes the following sequence of events possible:
numberOfRowsInSection is called before questions.removeAll, returning the old non-zero capacity
questions.removeAll clears questions
cellForRowAtIndexPath is called before questions are re-populated, causing index out of range exception
One way to fix is is relatively straightforward: make a newQuestions array, populate it in get_questions, and swap it in when numberOfRowsInSection is called:
// Add this to your class
var newQuestions : [questionItem]
// Change get_questions:
dispatch_async(dispatch_get_main_queue(), {
var json = JSON(data: data!)
if let items = json["questions"].array {
var tmpQuestions = [questionItem]()
for item in items {
let question = questionItem()
question.id = item["id"].int!
question.title = item["title"].string!;
question.question = item["question"].string!
tmpQuestions.append(question)
}
newQuestions = tmpQuestions
self.questionsTableView.reloadData()
}
});
// Change numberOfRowsInSection
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
if newQuestions != nil {
questions = newQuestions
newQuestions = nil
}
return questions.count
}
Note how get_questions does not populate newQuestions directly. Instead, it builds tmpQuestions, and sets it to newQuestions only when it is fully built.
Try with below code,
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell : QuestionsListCell = self.questionsTableView.dequeueReusableCellWithIdentifier("QuestionsListCell") as! QuestionsListCell
if let objModel : questionItem = questions[indexPath.row] as? questionItem
{
print(objModel.title)
}
}
Change your cellForRow with this method. Hope this helps you.
Just to avoid crash, I would have added following three safe checks.
1. Check Array count before clearing out.
if questions.count > 0
{
questions.removeAll()
}
2. Check array count before table Reload
if questions.count > 0
{
self.questionsTableView.reloadData()
}
3. In cellForRowAtIndex method Check for value in array of object, before putting on Cell
if let objModel : questionItem = questions[indexPath.row] as? questionItem
{
print(" title is \(objModel.title)")
}
I'm working on an iOS application using Swift and I'm using NSMutableArray, and when I try to add UISearchController to my UITableViewController It give me this error
1.1 click here, please
But when I try to do it with NSArray it works great.
If you wondering about Why am I using NSMutableArray?
Because I need it to pass the object of the NSMutableArray that is in the selected row from the UITableViewController to another UIViewController like this:
1.2 click here, please
What I have to do now, how can I adding search bar?
Thanks in advance.
You can hack it by converting the mutable array to a nsarray and then using that.
Where your doing the stuff in 1.1 just add before the the self.businessNamesArray line add this line
var regArray = self.businessNamesArray as NSArray as! [String]
then change the self.businessNamesArray line to regArray.filter{ rest of your code here
If you use regular Swift arrays, which I think is the right idea since you're using Swift, you can get it working if you change your array variable type and fix your array filtering syntax. First, change the array declaration to this:
var BusinessNamesArray:[String]?
The filter function your are using on your self.BusinessNamesArray returns a filtered array rather than filtering the array in place (which is what it seems like you want). If you want to replace the content of your self.BusinessNamesArray, you would need to do something like this:
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.BusinessNamesArray = self.BusinessNamesArray.filter { (business:String) -> Bool in
return true
// You probably want to compare strings here like this instead:
// guard let searchText = searchController.searchBar.text else {
// return false
// }
// return business.hasPrefix(searchText)
}
// You probably also need to reload your table view at this point:
self.tableView.reloadData()
}
Keep in mind though that your BusinessNamesArray is now filtered and you can't un-filter it. Instead, you should probably keep a second array of your Strings called something like searchResults. Then you can use that for filtering and maintain your original list of business names in the original array. So you would add a class variable called var searchResults:[String]? and the search filtering code would change to.
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.searchResults = self.BusinessNamesArray.filter { (business:String) -> Bool in
guard let searchText = searchController.searchBar.text else {
return false
}
return business.hasPrefix(searchText)
}
self.tableView.reloadData()
}
When you call reloadData on your table view at this point, you will probably want to check and see if your search controller is active in the table view delegate's numberOfSectionsInTableView as well other delegate functions and use the searchResults array if it is active and the BusinessNamesArray if its not--something like:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if (self.resultSearchController.active) {
return self.searchResults.count ?? 0
} else {
return self.BusinessNamesArray.count ?? 0
}
}
(The self.resultSearchController variable is a local instance of UISearchController. I'm not sure what you've named yours.)
You'll then use similar code to decide which items to grab for your prepareForSegue: code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Guard your variables here so you can return early if they're
// not valid
guard let upcoming = segue.destinationViewController as? DetailsViewController,
myindexpath = self.tableView.indexPathForSelectedRow else {
return
}
var titleString = ""
if (self.resultSearchController.active) {
if let searchResults = self.searchResults {
titleString = searchResults[myindexpath.row]
}
} else {
if let businessNames = self.BusinessNamesArray {
titleString = businessNames[myindexpath.row]
}
}
upcoming.titleString = titleString
self.deselectRowAtIndexPath(myindexpath, animated: true)
}
Attached at very bottom of this question is my inventory controller file. My problem is I'm getting duplicate results in all the sections. I narrowed down the reason to
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
My code in that function does not account for how many rows there are in each section. As such I'm just printing out the same duplicate results every section.
The actual question is listed after the images below...
Refer to images below:
I also have the ability to change the index from my settings menu so it can index by numbers, like 0-9. Refer to image below:
That said, I currently load the data from Core Data. Attached is reference image of the entities I use and there relationships.
The Question:
My question is, how can I get the results from coreData to be sorted into the A,B,C type sections or 1,2,3 sections so that navigating the table will be simple.
My hunch is the line that says let inventoryRecords = try moc.executeFetchRequest(inventoryFetchRequest) as? [Inventory] needs a sort descriptor to sort based on how I like, but how I then take the data and put into the correct array structure to split into the sections I need...I have no idea.
globals.swift
import Foundation
import CoreData
//Array of Inventory & Store Core Data Managed Objects
var g_inventoryItems = [Inventory]()
var g_storeList = [Store]()
var g_appSettings = [AppSettings]()
var g_demoMode = false
InventoryController.swift
import UIKit
import CoreData
class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var inventoryTable: UITableView!
var numberIndex = ["0","1","2","3","4","5","6","7","8","9"]
var letterIndex = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
var moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext //convinience variable to access managed object context
// Start DEMO Related Code
func createInventoryDummyData(number: Int) -> Inventory{
let tempInventory = NSEntityDescription.insertNewObjectForEntityForName("Inventory", inManagedObjectContext: moc) as! Inventory
tempInventory.name = "Test Item # \(number)"
tempInventory.barcode = "00000000\(number)"
tempInventory.currentCount = 0
tempInventory.id = number
tempInventory.imageLargePath = "http://website.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
tempInventory.imageSmallPath = "http://website.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
tempInventory.addCount = 0
tempInventory.negativeCount = 0
tempInventory.newCount = 0
tempInventory.store_id = 1 //belongs to same store for now
//Select a random store to belong to 0 through 2 since array starts at 0
let aRandomInt = Int.random(0...2)
tempInventory.setValue(g_storeList[aRandomInt], forKey: "store") //assigns inventory to one of the stores we created.
return tempInventory
}
func createStoreDummyData(number:Int) -> Store{
let tempStore = NSEntityDescription.insertNewObjectForEntityForName("Store", inManagedObjectContext: moc) as! Store
tempStore.address = "100\(number) lane, Miami, FL"
tempStore.email = "store\(number)#centraltire.com"
tempStore.id = number
tempStore.lat = 1.00000007
tempStore.lng = 1.00000008
tempStore.name = "Store #\(number)"
tempStore.phone = "123000000\(number)"
return tempStore
}
// End DEMO Related Code
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
print("InventoryController -> ViewDidLoad -> ... starting inits")
//First check to see if we have entities already. There MUST be entities, even if its DEMO data.
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
let storeFetchRequest = NSFetchRequest(entityName: "Store")
do {
let storeRecords = try moc.executeFetchRequest(storeFetchRequest) as? [Store]
if(storeRecords!.count<=0){
g_demoMode = true
print("No store entities found. Demo mode = True. Creating default store entities...")
var store : Store //define variable as Store type
for index in 1...3 {
store = createStoreDummyData(index)
g_storeList.append(store)
}
}
let inventoryRecords = try moc.executeFetchRequest(inventoryFetchRequest) as? [Inventory]
if(inventoryRecords!.count<=0){
g_demoMode = true
print("No entities found for inventory. Demo mode = True. Creating default entities...")
var entity : Inventory //define variable as Inventory type
for index in 1...20 {
entity = createInventoryDummyData(index)
g_inventoryItems.append(entity)
}
print("finished creating entities")
}
}catch{
fatalError("bad things happened \(error)")
}
print("InventoryController -> viewDidload -> ... finished inits!")
}
override func viewWillAppear(animated: Bool) {
print("view appearing")
//When the view appears its important that the table is updated.
//Look at the selected Store & Use the LIST of Inventory Under it.
inventoryTable.reloadData()//this is important to update correctly for changes that might have been made
}
// 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.
print("inventoryItemControllerPrepareForSegueCalled")
if segue.identifier == "inventoryInfoSegue" {
let vc = segue.destinationViewController as! InventoryItemController
if let cell = sender as? InventoryTableViewCell{
vc.inventoryItem = cell.inventoryItem! //sets the inventory item accordingly, passing its reference along.
}else{
print("sender was something else")
}
}
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
//This scrolls to correct section based on title of what was pressed.
return letterIndex.indexOf(title)!
}
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
//Use correct index on the side based on settings desired.
if(g_appSettings[0].indextype=="letter"){
return letterIndex
}else{
return numberIndex
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//TODO: Need to figure out how many rows for ...column A,B,C or 1,2,3 based on indexType using~
//To do this we need to organize the inventory results into a section'ed array.
if(g_appSettings[0].selectedStore != nil){
return (g_appSettings[0].selectedStore?.inventories!.count)! //number of rows is equal to the selected stores inventories count
}else{
return g_inventoryItems.count
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("InventoryTableCell", forIndexPath: indexPath) as! InventoryTableViewCell
if(g_appSettings[0].selectedStore != nil){
//Get the current Inventory Item & Set to the cell for reference.
cell.inventoryItem = g_appSettings[0].selectedStore?.inventories?.allObjects[indexPath.row] as! Inventory
}else{
//This only happens for DEMO mode or first time.
cell.inventoryItem = g_inventoryItems[indexPath.row]//create reference to particular inventoryItem this represents.
}
cell.drawCell() //uses passed inventoryItem to draw it's self accordingly.
return cell
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if(g_appSettings[0].indextype == "letter"){
return letterIndex[section]
}else{
return numberIndex[section]
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if(g_appSettings[0].selectedStore != nil){
if(g_appSettings[0].indextype=="letter"){
return letterIndex.count
}else{
return numberIndex.count
}
}else{
return 1//only one section for DEMO mode.
}
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//dispatch_async(dispatch_get_main_queue()) {
//[unowned self] in
print("didSelectRowAtIndexPath")//does not recognize first time pressed item for some reason?
let selectedCell = self.tableView(tableView, cellForRowAtIndexPath: indexPath) as? InventoryTableViewCell
self.performSegueWithIdentifier("inventoryInfoSegue", sender: selectedCell)
//}
}
#IBAction func BarcodeScanBarItemAction(sender: UIBarButtonItem) {
print("test of baritem")
}
#IBAction func SetStoreBarItemAction(sender: UIBarButtonItem) {
print("change store interface")
}
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
print("text is changing")
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
print("ended by cancel")
searchBar.text = ""
searchBar.resignFirstResponder()
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
print("ended by search")
searchBar.resignFirstResponder()
}
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
print("ended by end editing")
searchBar.resignFirstResponder()
}
#IBAction func unwindBackToInventory(segue: UIStoryboardSegue) {
print("unwind attempt")
let barcode = (segue.sourceViewController as? ScannerViewController)?.barcode
searchBar.text = barcode!
print("barcode="+barcode!)
inventoryTable.reloadData()//reload the data to be safe.
}
}
//Extention to INT to create random number in range.
extension Int
{
static func random(range: Range<Int> ) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
Update:: **
So I was looking around and found this article (I implemented it).
https://www.andrewcbancroft.com/2015/03/05/displaying-data-with-nsfetchedresultscontroller-and-swift/
I'm really close now to figuring it out. Only problem is I can get it to auto create the sections, but only on another field, like for example store.name, I can't get it to section it into A,B,C sections or 1,2,3.
This is my code for the fetchedResultsController using the methods described in that article.
//Create fetchedResultsController to handle Inventory Core Data Operations
lazy var fetchedResultsController: NSFetchedResultsController = {
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
let primarySortDescriptor = NSSortDescriptor(key: "name", ascending: true)
let secondarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
inventoryFetchRequest.sortDescriptors = [primarySortDescriptor, secondarySortDescriptor]
let frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "store.name",
cacheName: nil)
frc.delegate = self
return frc
}()
Question is what to put for sectionNameKeyPath: now that will make it section it on A B C and I got this !
Found a stackoverflow post very similar to my issue, but need swift answer.
A-Z Index from NSFetchedResultsController with individual section headers within each letter?
Here is another similar article but all objective-c answers.
NSFetchedResultsController with sections created by first letter of a string
Update::
Found another article I think with my exact issue (How to have a A-Z index with a NSFetchedResultsController)
Ok I figured it out, phew was this confusing and took a lot of research.
Okay, so first thing you have to do is create a transient property on the data model. In my case I called it lettersection. To do this in the entity just create a new attribute and call it lettersection and in graph mode if you select it (double click it), you will see option in inspector for 'transient'. This means it won't be saved to the database and is used more for internal reasons.
You then need to manually set up the variable in the extension area of the model definition. Here is how it looks for me.
import Foundation
import CoreData
extension Inventory {
#NSManaged var addCount: NSNumber?
#NSManaged var barcode: String?
#NSManaged var currentCount: NSNumber?
#NSManaged var id: NSNumber?
#NSManaged var imageLargePath: String?
#NSManaged var imageSmallPath: String?
#NSManaged var name: String?
#NSManaged var negativeCount: NSNumber?
#NSManaged var newCount: NSNumber?
#NSManaged var store_id: NSNumber?
#NSManaged var store: Store?
var lettersection: String? {
let characters = name!.characters.map { String($0) }
return characters[0].uppercaseString
}
}
Once you do this, you simply call this new 'lettersection' with the fetchedResultsController like so...
let frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "lettersection",
cacheName: nil)
and everything will work! It sorts by the name of my inventory items, but groups them by the first letters, for a nice A,B,C type list!
"My question is, how can I get the results from coreData to be sorted into the A,B,C type sections or 1,2,3 sections so that navigating the table will be simple."
Using "Store" as your entity and property "name" to be what you want to sort the records by.
override func viewDidLoad() { super.viewDidLoad()
let fetchRequest = NSFetchRequest()
let entity = NSEntityDescription.entityForName("Store", inManagedObjectContext: managedObjectContext)
fetchRequest.entity = entity
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let foundObjects = try managedObjectContext.executeFetchRequest(fetchRequest)
locations = foundObjects as! [Location]
} catch {
fatalCoreDataError(error) }
}
You are going to use this function to set the number of sections:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return letterindex.count // if that is how you want to construct sections
}
I learned this from the Ray Wenderlich e-book "iOS Apprentice". From Lesson 3 - MyLocations. Highly recommend this and their e-book book on CoreData.
Please help! I've tried everything. If anyone has any advice on how i can display my data in the table view cell, I would be eternally grateful. I'm new to iOS and am learning on a very steep pace. I grabbed data from an API that returned data in the form of JSON, parsed it, created my table view with its table view cells, but i can't seem to figure out how to print the data i parsed through in the table view cell.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var myTableView: UITableView! {
didSet {
myTableView.dataSource = self
myTableView.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
let url = NSURL(string: "https://api.viacom.com/apiKey=someKey")!
let request = NSMutableURLRequest(URL: url)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
if let response = response, data = data {
var json: [String: AnyObject]!
do {
json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! [String : AnyObject]
} catch {
print(error)
}
//2 - Store in model, forloop through them, store into temparray,add to main array?
let episodes = json["response"] as! [String: AnyObject]
let meta = episodes["episodes"] as! [AnyObject]
let description = meta[2]["description"]! as! String?
//let title = meta[2]["title"] as! String?
let episodeNumber = meta[2]["episodeNumber"]! as! String?
dispatch_async(dispatch_get_main_queue(), {
self.myTableView.reloadData()})
data = [episodeNumber!, description!]
print("Episode Number: \(episodeNumber!)\n" + "Description: \(description!)")
} else {
print(error)
}
}
task.resume()
}
let data = [description]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel!.text = "\(self.data)"
return cell
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Your codes look very messy to me. However, I'm just assuming that you have successfully fetched the JSON data. Fetching data is asynchronous. You therefore need to add a dispatch code inside.
After your this line of code:
let episodeNumber = meta[2]["episodeNumber"]! as! String?
Add this
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()})
EDIT:
#IBOutlet weak var myTableView: UITableView! {
didSet {
myTableView.dataSource = self
myTableView.delegate = self // Add This
}
}
The reason for the failure is too much of data manipulation. There is no need to use so many variables and pass around data unnecessarily. You are getting correct output in console when printing it because you used variables "episodeNumber" and "description".
print("Episode Number: \(episodeNumber!)\n" + "Description: \(description!)")
And getting wrong data in variable "data".
So better thing would be that you should use episodeNumber and description variables to print data in Cell.
cell.textLabel!.text = "Episode Number: \(self.episodeNumber)\n" + "Description: \(description)"
But for this you have to make variable episodeNumber a global variable.
So declare it outside the function.
var episodeNumber = String()
and remove the let keyword from line
let episodeNumber = meta[2]["episodeNumber"]! as! String?
You have to add some self. keywords which the compiler will suggest you so you don't have to worry about that, just keep on double clicking the suggestions.
Now, your code looks fine to run and get desired output.
let data = [description]
is a short form of
let data = [self.description]
and self.description() is the viewController's description method used for printing debug description. That is why
cell.textLabel!.text = "\(self.data)"
gives you [(Function)], as you just created an array with a stored function in it.
I'm quite new to working with Parse and I'm building a todo list as part of a CRM. Each task in the table view shows the description, due date, and client name. The description and due date are in my Task class, as well as a pointer to the Deal class. Client is a string in the Deal class. I'm able to query the description and due date properly, but I am not able to retrieve the client attribute from within the Deal object by using includeKey. I followed the Parse documentation for includeKey.
The description and due date show up properly in the resulting table view, but not the client. The log shows client label: nil and the printed task details include <Deal: 0x7ff033d1ed40, objectId: HffKOiJrTq>, but nothing about the client attribute. How can I retrieve and assign the pointer object's attribute (client) to my label within the table view? My relevant code is below. Thank you in advance.
Edit: I've updated my code with func fetchClients() based on this SO answer, but I'm still not sure whether my function is complete or where to call it.
class TasksVC: UITableViewController {
var taskObjects:NSMutableArray! = NSMutableArray()
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
println("\(PFUser.currentUser())")
self.fetchAllObjects()
self.fetchClients()
}
func fetchAllObjects() {
var query:PFQuery = PFQuery(className: "Task")
query.whereKey("username", equalTo: PFUser.currentUser()!)
query.orderByAscending("dueDate")
query.addAscendingOrder("desc")
query.includeKey("deal")
query.findObjectsInBackgroundWithBlock { (tasks: [AnyObject]!, error:NSError!) -> Void in
if (error == nil) {
var temp:NSArray = tasks! as NSArray
self.taskObjects = temp.mutableCopy() as NSMutableArray
println(tasks)
self.tableView.reloadData()
} else {
println(error?.userInfo)
}
}
}
func fetchClients() {
var task:PFObject = PFObject(className: "Task")
var deal:PFObject = task["deal"] as PFObject
deal.fetchIfNeededInBackgroundWithBlock {
(deal: PFObject!, error: NSError!) -> Void in
let client = deal["client"] as NSString
}
}
//MARK: - Tasks table view
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.taskObjects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("TaskCell", forIndexPath: indexPath) as TaskCell
var dateFormatter:NSDateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "M/dd/yy"
var task:PFObject = self.taskObjects.objectAtIndex(indexPath.row) as PFObject
cell.desc_Lbl?.text = task["desc"] as? String
cell.date_Lbl.text = dateFormatter.stringFromDate(task["dueDate"] as NSDate)
cell.client_Lbl?.text = task["client"] as? String
var clientLabel = cell.client_Lbl?.text
println("client label: \(clientLabel)")
return cell
}
}
If the deal column is a pointer then includeKey("deal") will get that object and populate it's properties for you. There is no need to perform a fetch of any type on top of that.
You really should be using Optionals properly though:
if let deal = task["deal"] as? PFObject {
// deal column has data
if let client = deal["client"] as? String {
// client has data
cell.client_Lbl?.text = client
}
}
Alternatively you can replace the last if let with a line like this, which handles empty values and uses a default:
cell.client_Lbl?.text = (deal["client"] as? String) ?? ""
In your posted cellForRowAtIndexPath code you are trying to read client from the task instead of from the deal: task["client"] as? String.