Swift Uisearchcontroller Wired Up in Core Data Project, App Runs, But Search Not Updating

Swift UISearchController wired up in Core Data Project, app runs, but search not updating

The problems lie in your tableView datasource methods: numberOfSectionsInTableview:, tableView:numberOfRowsInSection:, and tableView:cellForRowAtIndexPath:. You need each of those methods to return different results if the searchPredicate is not nil - much like your tableView:commitEditingStyle: method does. I would make filteredObjects an instance property (defined at the start of the class) so that all those methods can access it:

var filteredObjects : [Note]? = nil

Now, when the search text changes, you want to rebuild the filteredObjects array. So in updateSearchResultsForSearchController, add a line to recompute it based on the new predicate:

    if let searchText = searchText {
searchPredicate = NSPredicate(format: "noteBody contains[c] %@", searchText)
filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
} as [Note]?
self.tableView.reloadData()
println(searchPredicate)
}

I would also recommend (for simplicity) that when you activate the search, the results are displayed all in one section (otherwise you have to work out how many sections your filtered results fall into - possible but tiresome):

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if searchPredicate == nil {
return self.fetchedResultsController.sections?.count ?? 0
} else {
return 1
}
}

Next, if the searchPredicate is not nil, the number of rows in the section will be the count of filteredObjects:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.searchPredicate == nil {
let sectionInfo = self.fetchedResultsController.sections![section] as! NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
} else {
return filteredObjects?.count ?? 0
}
}

Finally, if the searchPredicate is not nil, you need to build the cell using the filteredObjects data, rather than the fetchedResultsController:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
if searchPredicate == nil {
self.configureCell(cell, atIndexPath: indexPath)
return cell
} else {
// configure the cell based on filteredObjects data
...
return cell
}
}

Not sure what labels etc you have in your cells, so I leave it to you to sort that bit.

Deleting from UISearchController's filtered search results

The problem arises because of a mismatch between the indexPath used by the fetched results controller and the indexPath for the corresponding row in the tableView.

Whilst the search controller is active, the existing tableView is reused to display the search results. Hence your logic to differentiate the two tableViews:

if searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
}

is unnecessary. It works, because you set searchController.searchResultsUpdater = self when you initialise the searchController, so there is no need to change it, but the same tableView is used in either case.

The difference lies in the way the tableView is populated whilst the searchController is active. In that case, it looks (from the numberOfRowsInSection code) as though the filtered results are all displayed in one section. (I assume cellForRowAtIndexPath works similarly.) Suppose you delete the item at section 0, row 7, in the filtered results. Then commitEditingStyle will be called with indexPath 0-7, and the following line:

let location: Location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location

will try to get the object at index 0-7 from the FRC. But the item at index 0-7 of the FRC might be a completely different object. Hence you delete the wrong object. Then the FRC delegate methods fire, and tell the tableView to delete the row at index 0-7. Now, if the object really deleted was NOT in the filtered results, then the count of rows will be unchanged, even though a row has been deleted: hence the error.

So, to fix it, amend your commitEditingStyle so that it finds the correct object to delete, if the searchController is active:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
var location : Location
if searchPredicate == nil {
location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
} else {
let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
}
location = filteredObjects![indexPath.row] as Location
}
location.removePhotoFile()

let context = self.fetchedResultsController.managedObjectContext
context.deleteObject(location)

var error: NSError? = nil
if !context.save(&error) {
abort()
}
}
}

I haven't been able to test the above; apologies if some errors slipped in. But it should at least point in the right direction; hope it helps. Note that similar changes may be required in some of the other tableView delegate/datasource methods.

Turning objects filtered from managedObjectContext into an NSArray

The managed object context has nothing to do with an array. It cannot be "filled" with objects. It is simply an abstract class that gives you access to your data.

The way you get objects from your Core Data store is as follows: make a fetch request, optionally add filter and/or sorting to the request, fetch the objects.

let request = NSFetchRequest(entityName:"Note")
let result = managedObjectContext.executeFetchRequest(request error:nil) as [Note]

You can now either filter this array (with filteredArrayUsingPredicate after casting to NSArray) or you can add your predicate to the original fetch request (which should even be more efficient).

How to change viewController based on device orientation

The reason for those warnings or errors is that there is a delay when dismissing and presenting view controllers and your calls to present and dismiss are overlapping with each other.

The first error reporting that the view is not in the hierarchy means that you are attempting to present a view on your original view controller or navigation controller that is no longer available because another view controller has been modally presented.

The second error regarding loading a view while it is deallocating indicates that the view controller dismissal has not fully completed when another modal presentation is being attempted.

To prevent these problems requires ensuring the dismissals are complete before presenting a new view controller. This can be accomplished by handling the states using the completion handler in the dismiss methods and adding a check on the isBeingDismissed state.

Here is some code that accomplishes that:

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

let storyboard = UIStoryboard(name: "Main", bundle: nil)

if UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation) {
let landscapeVC = storyboard.instantiateViewControllerWithIdentifier("landscapeViewController") as! beerCollectionViewController

if presentedViewController != nil {
if !presentedViewController!.isBeingDismissed() {
dismissViewControllerAnimated(false, completion: {
self.presentViewController(landscapeVC!, animated: true, completion: nil)
})
}
} else {
self.presentViewController(landscapeVC!, animated: true, completion: nil)
}
} else { // Portrait
let portraitVC = storyboard.instantiateViewControllerWithIdentifier("portraitViewController") as! ViewController

if presentedViewController != nil {
if !presentedViewController!.isBeingDismissed() {
dismissViewControllerAnimated(false, completion: {
self.presentViewController(portraitVC!, animated: true, completion: nil)
})
}
} else {
self.presentViewController(portraitVC!, animated: true, completion: nil)
}
}
}


Related Topics



Leave a reply



Submit