Filter viewController - swift

In my app I have a search bar where people can add free text and get the results the meet the criteria
func search(searchClinics: [Clinica], searchArr: [String]) -> [Clinica] {
var searchArr = searchArr
// base case - no more searches - return clinics found
if searchArr.count == 0 {
return searchClinics
}
// itterative case - search clinic with next search term and pass results to next search
let foundClinics = searchClinics.filter { item in
(item.name.lowercased() as AnyObject).contains(searchArr[0]) ||
item.specialty1.lowercased().contains(searchArr[0]) ||
item.specialty2.lowercased().contains(searchArr[0])
}
// remove completed search and call next search
searchArr.remove(at: 0)
return search(searchClinics: foundClinics, searchArr: searchArr)
}
I also have a flag to identify if the searchBar is being used
var searching = false // Checks if searchBar was used
In case searchBar was used, it returns the filtered array data otherwise returns the full list. For example
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
let totalClinics = clinicsSearch.count
if totalClinics == 0 {
return 1
} else {
return totalClinics
}
} else {
let totalClinics = clinics.count
if totalClinics == 0 {
return 1
} else {
return totalClinics
}
}
}
I'm now willing to add another viewController where the user would be able to define specific filters (like State, City, Specialty among others). Once the user clicks apply, it would go back to the previous viewController with filtering data.
I'm in doubt with the best approach to apply this filter. At first I though about doing something like:
User clicks a button in the navigationBar and opens "Search" viewController;
User inputs Data;
User clicks "Apply";
Call previous viewController;
I would add another status flag with status "True" that will be used in my tableView ViewController. If true, considers the list of clinics with filters applied. If not, show the full list of clinics;
I've a lot of searching in stackoverflow but I found a lot of filtering/searchbar stuff but none related to a separate search/filter viewController.
Would you recommend this approach or is there a better way to do it?
One of my concerns here if with step 4... if I call a segue, wouldn't I be stacking views and consuming more memory?
Thanks

In my app user click filter button present Controller open
let vc = storyboard?.instantiateViewController(withIdentifier: "searchFilter") as! SearchFilter
vc.modalPresentationStyle = .overCurrentContext
vc.SearchCompletion = {(model,flag) in
if(flag){
self.serachArr.removeAllObjects()
for i in 0..<model.count{
self.serachArr.add(ListModel(data: model[i] as! NSDictionary))
}
self.ListTable.reloadData()
}
}
self.present(vc, animated: true, completion: nil)
User click apply button and pass data previous Viewcontroller
self.SearchCompletion(dataArr,true)

Related

fatal errors with optionals not making sense

I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.
I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}
Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).

The correct methodology for UICollectionView filtering

I just implemented a filtering mechanism in my UICollectionView and I think this is not the correct methodology I should have chosen.
How can I improve my code to make it have better performance?
I have a screen where you input your criterias for filtering, and then this code handles the rest:
extension ProductsCollectionViewController
{
#objc func Filter()
{
performSegue(withIdentifier: "segue_to_filter_view", sender: self)
}
#IBAction func unwindFromFilterScreenAccept(segue: UIStoryboardSegue)
{
if segue.source is FilterViewController
{
if let senderVC = segue.source as? FilterViewController
{
if senderVC.chosenCategory != "" {categoryFilter = senderVC.chosenCategory}
self.areFiltersSet = true
}
}
self.tabBarController?.tabBar.isHidden = false
self.products.removeAll()
self.LoadProducts(productsToShow: pageType)
//self.LoadProducts(productsToShow: pageType)
}
#IBAction func unwindFromClearAllFilters(segue: UIStoryboardSegue)
{
ClearAllFilters()
self.products.removeAll()
self.LoadProducts(productsToShow: pageType)
}
private func ClearAllFilters()
{
areFiltersSet = false
}
private func FilterProduct(prod: Product) -> Bool
{
if areFiltersSet == false {return true}
return FilterProductByCategory(prod:prod) && FilterProductByLocation(prod:prod) && FilterProductByCondition(prod:prod)
}
Also:
I have a search bar implemented. Will the two work together? Is there a way to properly integrate the two?
P.S:
The Filtering itself is checked when adding a product to the collection when loading products.
Filtering by performing a segue is not the best way. It will be better to update the collectionView itself depending on the filter that you choose. For example, if you have products that correspond to a specific category you can just do something like:
let products = [Product(category: "one"), Product(category: "one"), Product(category: "two")]
let foo = products.filter {$0.category == "one"}
that filter function will return only the products that correspond to category one. Then you can just use the filtered array to populate the collectionView.
About the searchBar, yes, they will work together with any other filter function. Basically you will just perform two filters:
Based on the category property
Based on the substring that you input
Just for future reference, if you add the searchBar as collectionViewCell or as header you will HAVE to reload items with the indexPaths because reloadData() will resign the responder from the searchBar.
One more thing, just as a suggestion, it is a common practice to name your methods with lowercase -> filter() instead of Filter().

How to filter table view cells with a UISegmentedControl in Swift?

I've already searched this before asking the question but I didn't find what I need.
I'm building this app where the user puts a task (not going to the app store, just for me and some friends), and the task has a category. For example: school, home, friends, etc. When the user is going to add a new task, there are 2 text fields, the description text field and the category text field. I'm using a UIPickerView so the user picks a category, then, after creating the new task, it will add the category to an array I've created called "categories".
I want to put an UISegmentedControl on top of the table view with the sections:
All - School - Home - Friends
If all is selected, it will show all the cells with no filtering. If not, it will show the cell(s) with the corresponding categories.
I've read that I need to create table view sections to each category, but this would change my code a lot, and I don't even have an idea of how to work with multiple table view sections, I've tried once but it kept repeating the cells of one section in the second.
So how can I filter the cells per category?
Can I just put for example this? :
if //code to check in which section the picker is here {
if let schoolCell = cell.categories[indexPath.row] == "School" {
schoolCell.hidden = true
}
}
Please help me!!!
EDIT:
I have this code by now:
if filterSegmentedControl.selectedSegmentIndex == 1 {
if categories[indexPath.row] == "School" {
}
}
I just don't know where to go from here. How do I recognize and hide the cells?
It seems to me that you may want to take a simpler approach first and get something working. Set up your ViewController and add a tableView and two(2) arrays for your table data. One would be for home and the other for work. Yes, I know this is simple but if you get it working, then you can build on it.
Add a variable to track which data you are displaying.
#IBOutlet var segmentedControl: UISegmentedControl!
#IBOutlet var tableView: UITableView!
// You would set this to 0, 1 or 2 for home, work and all.
var dataFilter = 0
// Data for work tasks
var tableDataWork : [String] = ["Proposal", "Send mail", "Fix printer", "Send payroll", "Pay rent"]
// Data for home tasks
var tableDataHome : [String] = ["Car payment", "Mow lawn", "Carpet clean"]
Add these functions for the segmented control.
#IBAction func segmentedControlAction(sender: AnyObject) {
switch segmentedControl.selectedSegmentIndex {
case 0:
print("Home")
dataFilter = 0
case 1:
print("Work")
dataFilter = 1
case 2:
print("All")
dataFilter = 2
default:
print("All")
dataFilter = 2
}
reload()
}
func reload() {
dispatch_async( dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("task-cell")
var title: String?
switch dataFilter {
case 0:
title = tableDataHome[indexPath.row]
case 1:
title = tableDataWork[indexPath.row]
case 2:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row - tableDataWork.count]
}
default:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row + tableDataWork.count]
}
}
cell?.textLabel?.text = title
if cell != nil {
return cell!
}
return UITableViewCell()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If
switch dataFilter {
case 0: return tableDataHome.count
case 1: return tableDataWork.count
default: return tableDataHome.count + tableDataWork.count
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1;
}
You can find the entire project here: https://github.com/ryantxr/segmented-control-app
It depends on your tableview.
If you use NSFetchedResultsController then you need to modify your fetch request. If you use an array directly, just use the filter function in Swift, passing in the condition, e.g. filteredArray = array.filter{$0.isAudioFile} Then, after setting your datasource array to the filtered one, call reloadData on your tableview.
You will need to keep a reference to the full array, and use the filtered one as your datasource in cellForRow...

How to update user interface on Core Data?

I have an app with UITableView, Core Data and NSFetchedResultsController as well. I have passed data to the DetailViewController. And I can delete them from the DetailViewController! In the Apple's iOS Notes app, you can see such as functions as I wanted! When you delete a notes from the DetailViewController ( for example ), object deleted and Notes app automaticlly shows the next or previos notes! I want to create such as function. How update user interface after deleted current object? Here's my codes! Thanks `
import UIKit
import CoreData
class DetailViewController: UIViewController {
#IBOutlet weak var containerLabel: UILabel!
var retrieveData:NSManagedObject!
var managedObjectContext:NSManagedObjectContext!
var manager:Manager!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.containerLabel.userInteractionEnabled = false
self.containerLabel.textColor = UIColor.redColor()
self.containerLabel.alpha = 0
UIView.animateWithDuration(2.5) { () -> Void in
self.containerLabel.alpha = 1
}
if let demo = self.retrieveData.valueForKey("titleField") as? String {
self.containerLabel.text = demo
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func backToMain(sender: AnyObject) {
// Back to the MainTableViewController
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func trashButton(sender: AnyObject) {
self.managedObjectContext.deleteObject(retrieveData)
do {
try self.managedObjectContext.save()
} catch {
}
self.dismissViewControllerAnimated(true, completion: nil)
}
`
If I have 5 items on the list like so:
When I select fourth item from the list ( for example ). And detailVC shows me selected item like this:
And I want to delete them. When I delete "Four" and then my containerLabel.text shows previous objects from the list. They're after "Four" is deleted, "Three","Two" and "One" as well. After "One" is deleted my containerLabel.text shows strings
But I have left single object called as "Five"
My problem is "Five"! I can't delete it. Example: In iOS Notes App, if you have five objects on the list like my demo app. When you select fourth object from the list ( for example ). And begin deleting them, after "Four" is delete iOS Notes App shows "Five". And "Five" ( last object on the list ) is deleted and then iOS Notes App shows "Three", "Two" and "One". Maybe problem line is here:
if index != 0 {
self.retrieveData = fetchedObject[index! - 1]
} else {
self.retrieveData == fetchedObject[0]
}
Let's take the easy (but not so elegant) route here. You'll have to pass over all the fetched objects to the detail VC like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "yourSegueIdentifier"{
if let destinationVC = segue.destinationViewController as? DetailViewController{
destinationVC.managedObjectContext = yourContext
destinationVC.retrieveData = yourManagedObject
destinationVC.arrayOfFetchedObjects = yourFetchedResultsController.fetchedObjects
//pass over other data...
}
}
}
Then, in your detailVC, write a method that will be executed when you press the delete button. Something like this:
#IBAction func trashButton(sender: AnyObject) {
//make sure you have an array with YourObjects
guard let fetchedObjects = arrayOfFetchedObjects as? [YourObjectType] else {return}
//get index of the shown object in the array of fetched objects
let indexOfObject = fetchedObjects.indexOf(retrieveData)
//delete the object from the context
self.managedObjectContext.deleteObject(retrieveData)
do {
try self.managedObjectContext.save()
//delete the object from the fetchedObjects array
fetchedObjects.removeAtIndex(indexOfObject)
} catch {
}
//get the object that should be shown after the delete
if indexOfObject != 0{
//we want the object that represents the 'older' note
retrieveData = fetchedObjects[indexOfObject - 1]
updateUserInterface(true)
}
else{
//the index was 0, so the deleted object was the oldest. The object that is the oldest after the delete now takes index 0, so just use this index. Also check for an empty array.
if fetchedObjects.isEmpty{
updateUserInterface(false)
}
else{
retrieveData = fetchedObjects[0]
updateUserInterface(true)
}
}
}
func updateUserInterface(note: Bool){
switch note{
case true:
//update the user interface
if let demo = retrieveData.valueForKey("titleField") as? String {
self.containerLabel.text = demo
}
case false:
self.containerLabel.text = "no more notes"
}
}
You either need to pass the details view controller
A list of all managed objects and an index for where in the list to start
A current managed object and a callback to get the next object
In order for it to have enough information to do what you want. The callback approach is nicest and is a simple form of delegate, where your master view controller is the delegate supplying the extra data.

Storing Individual UISwitch States in UITableView

I have a UITableView that has Two ProtoType Cells both with separate TableViewCell subclassess. In one of the prototype cells I have multiple switches. The user can select the item they want in the table and turn the switch on that corresponds to the item. I would like to be able to store the UISwitchstate so if the user navigates away and comes back they will see what they have selected previously.
I'm trying to store the UISwitchstate in a dictionary and then call the state back when the table gets reloaded.
Here is the code I have so far:
#IBAction func switchState(sender: AnyObject) {
if mySwitch.on{
savedItems = NSMutableDictionary(object: mySwitch.on, forKey: "switchKey")
standardDefaults.setObject("On", forKey: "switchKey")
}
else{
standardDefaults.setObject("Off", forKey: "switchKey")
and then this is in the awakeNib section:
override func awakeFromNib() {
super.awakeFromNib()
self.mySwitch.on = NSUserDefaults.standardUserDefaults().boolForKey("switchKey")
NSUserDefaults.standardUserDefaults().registerDefaults(["switchKey" : true])
}
thanks for the help.
What you need to do is:
Having a dictionary (or any other data type) that is from the exact same size as your data source.
And in your
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
you need to that dictionary for the row indexPath.row and then mainpulate your cell
Note, your dictionary should be static
Putting my answer here instead of a comment so I can hopefully explain things better. You need to have some sort of system to keep up with which dictionary item corresponds with which UISwitch. Your best bet would probably be to have a dictionary var uiDictionary = [String : Bool]() where your keys are a string that you know corresponds to a specific switch. Then, in your cellForRowAtIndexPath: you would try to access each dictionary item, check if it's the one for the switch you're trying to set, and then set it. Don't know your exact setup, but it would look something like this...
func cellForRowAtIndexPath() {
//other stuff here
//now set your switches
for (key, value) in uiDictionary {
switch(key) {
case "Switch1":
if value == true {
cell?.switch1.setOn(true, animated: true)
} else {
cell?.switch1.setOn(false, animated: true)
}
break
case "Switch2":
if value == true {
cell?.switch1.setOn(true, animated: true)
} else {
cell?.switch1.setOn(false, animated: true)
}
}
}
}