Implement searchbar to tableview with custom cell - ios

I created a tableview with custom cell and im struggling now to add a searchbar.
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
var SVGdata = [SVG]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
createSVGdata()
}
// Create movies data
func createSVGdata() {
SVGdata.append(SVG(SVGArtikel: "Art. 1", SVGGesetzestext: "Das darfst du nicht tun"))
SVGdata.append(SVG(SVGArtikel: "Art. 2", SVGGesetzestext: "Und das erst recht nicht"))
}
// TableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return SVGdata.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SVGCell", for: indexPath)
let sVG = SVGdata[indexPath.row]
if let SVGCell = cell as? SVGCell {
SVGCell.setSVG(sVG)
}
return cell
}
}
There are thousands of videos how to search in a simple array. But not for my solution.

You can do this using UISearchBar.
First add a UISearchBar in story board on the same viewController where your tableView is (Add the search bar on top of table view like iOS. Contact app) then add reference like below code:
#IBOutlet weak var searchBar: UISearchBar!
Then in viewDidLoad add delegate :
self.searchBar.delegate = self
I have two dataArray allData, filteredData. in allData I have kept all data of the table(Didn’t change it anywhere) and filteredData used to get the filtered result.
So, after populating data into allData , I have done from (viewWillAppear):
filteredData = allData
So , All tableView delegates, datasources is used the filteredData.
Then make extension of your viewController (making extension is not mandatory ) to conform UISearchBarDelegate :
As you use SVGdata( I used filteredData) for tableView, after filtering you have to put all filtered data into SVGdata before reloading the table.
extension YourViewController: UISearchBarDelegate{
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// searchText will contain user input
// in allData I have kept all data of the table
// filteredData used to get the filtered result
if (searchText == ""){
filteredData = allData
}
else{
filteredData = []
// you can do any kind of filtering here based on user input
filteredData = allData.filter{
$0.cName.lowercased().contains(searchText.lowercased())
}
}
// lastly you need to update tableView using filteredData so that the filtered rows are only shown
//As you use SVGdata for tableView, after filtering you have to put all filtered data into SVGdata before reloading the table.
self.tableView.reloadData()
}
}

I know two ways of adding a search bar when you have a TableView, so I recommend you to decide first which implementation suits better for your app:
First one, if you have a UINavigationController, you can use a native implementation of the searchbar that will appear in the navigation bar. I mean, you first need a UINavitationController added to your view controller, programmatically or through storyboard. Then, you should create a UISearchController and associated to the UINavigationController. Lastly, listen for events through UISearchController Delegate occurring in navigation search bar so you can update you view.
https://www.raywenderlich.com/472-uisearchcontroller-tutorial-getting-started
The second way is a searchbar which is associated to a tableView through his header, read several tutorials until you make it.
https://www.ioscreator.com/tutorials/add-search-table-view-ios-tutorial

Related

Call reloadData() to refresh TableView - Reusable Code

I need the tableView to perform reloadData() after a row has been added via a textView. My tableViews all use the reusable code which works fine.
Below is my Reusable TableViewCode.
class ReusableSubtitleTable: NSObject, UITableViewDataSource, UITableViewDelegate{
let cell = "cell"
var dataArray = [String]()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) {
print("DataArray count from table view = \(dataArray.count)")
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let selfSizingCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SelfSizingCell
let num = indexPath.row
selfSizingCell.titleText.text = (stepText[num])
selfSizingCell.subtitleText.text = dataArray[num]
return selfSizingCell
}
}
The function below uses the reusable code to display the table which works fine.
class DetailViewController: UIViewController {
let step = 13
var tableView: UITableView!
let dataSource = ReusableSubtitleTable()
var selectedEntry: JournalEntry!
var dataModel = [String]()
var didSave = false
var coreDataManager = CoreDataManager()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = dataSource
tableView.dataSource = dataSource
dataSource.dataArray = dataModel
#IBAction func unwindToDetail( _ sender: UIStoryboardSegue) {
dataModel[10] = step11
didSave = coreDataManager.updateEntry(step11: step11, selectedEntry: selectedEntry)
}
}
The problem come in when a user wants to add to the last row. The user taps a button and is taken to the next controller which is a TextView. When user finishes their entry they tap the 'Save' button which returned to the DetailViewController via an unwind. The selectedEntry is saved and the dataModel updated. Now the table view needs to reload to display this added text.
I've tried adding tableView.ReloadData() after didSave. I've tried a Dispatch and tried saving the data before returning from the textView via the unwind but that doesn't work either.
I tried adding the below function to ReusableTableView and called it after the coredata update - there are no errors but it does not reload the table.
func doReload(){
tableView.reloadData()
}
I have verified that the data is saved and it does displays if I return to the summary controller and then go forward the DetailViewController.
Any help is appreciated.
Placing the UITableView reloadData() within either viewWillAppear or viewDidAppear should resolve this issue.
For example:
func viewDidAppear(_ animated: bool) {
super.viewDidAppear(animated)
tableView.reloadData()
}
This is because the view hierarchy isn't yet regarded as "visible" during the segue unwind and why you see it work by going back to reloading the view controller. The reloadData() function, for efficiency, only redisplays visible cells and at the time of the unwind the cells aren't "visible".
Apple Documentation - UITableView.reloadData():
For efficiency, the table view redisplays only those rows that are
visible.

How to add multiple data to a UITableView from a UITextField? (SWIFT 4)

I am trying to create a program on Xcode that allows the user to enter multiple data into a table view through a text field (when a button is clicked). When the data is added I would like it to be stored and not be deleted after the app is closed - for this part I believe that I would have to use NSUserDefaults, however, I am unsure how I would save an array of strings? (I'm only familiar with storing a single string).
This is what my view controller currently looks like.
I have not done much on my view controller at all but this is what it currently has.
import UIKit
class NewViewController: UIViewController {
#IBOutlet weak var text: UITextField!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
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 prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Let's tackle this step-by-step...
TL;DR - For your convenience, I've put the final code into a sample project on Github. Feel free to use any or all of the code in your apps. Best of luck!
Step 1 - Conform to UITableView Protocols
"...enter multiple data into a table view..."
At a minimum, UITableView requires you to conform to two protocols in order to display data: UITableViewDelegate and UITableViewDataSource. Interface Builder handles the protocol declaration for you if you use the built-in UITableViewController object, but in your case you cannot use that object because you only want the UITableView to take up a portion of the view. Therefore, you must implement the protocols yourself by adding them to ViewController's signature:
Swift 4
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
}
Step 2 - Implement UITableView Protocol Required Methods
Now that you have the protocols declared, Xcode displays an error until three required methods are implemented inside of your ViewController class. The bare minimum implementation for these methods is:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
You'll implement these methods later, but at this point your code should compile.
Step 3 - Connect UITableView's Protocols to ViewController
Since you are using a standard UITableView object, ViewController is not connected by default to the code you just implemented in the protocol methods. To make a connection, add these lines to viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Alternatively, you could use the CONTROL + DRAG technique in Interface Builder to connect the delegate and data source from your UITableView to ViewController.
NOTE: In this case, self refers to the ViewController since you're inside of the ViewController class.
Step 4 - UITextField Setup
"...through a text field..."
You previously added an IBOutlet for your UITextField that is connected to Interface Builder, so there is nothing more to do here.
Step 5 - IBAction for the Add Button
(when a button is clicked)."
You need to add an IBAction to your ViewController class and connect it to your Add Button in Interface Builder. If you prefer to write code and then connect the action, then add this method to your ViewController:
#IBAction func addButtonPressed(_ sender: UIButton) {
}
If you use Interface Builder and the CONTROL + DRAG technique to connect the action, the method will be added automatically.
Step 6 - Add an Array Property to Store Data Entries
"...save an array of strings..."
You need an array of strings to store the user's entries. Add a property to ViewController that is initialized as an empty array of strings:
var dataArray = [String]()
Step 7 - Finish Implementing UITableView Protocol Methods
At this point you have everything you need to finish implementing UITableView's protocol methods. Change the code to the following:
//1
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Do nothing
}
//2
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
//3
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataArray[indexPath.row]
return cell
}
In the future, if you want to do something when the user taps a cell, you will want to add code to tableView(_:didSelectRowAt:).
You now create the same number of rows as the number of values in dataArray.
To make this work with Interface Builder, make sure you go to the Attributes Inspector for your UITableViewCell and set the Cell Identifier to Cell. Check out the documentation for more on Dequeuing Cells.
Step 8 - Finish Implementing addButtonPressed(_:)
As suggested in #dani's answer, in the action you need to implement code that appends the user's text to the array, but only if the text is not blank or empty. It is also a good idea to check if dataArray already contains the value you entered using dataArray.contains, depending on what you want to accomplish:
if textField.text != "" && textField.text != nil {
let entry = textField.text!
if !dataArray.contains(entry) {
dataArray.append(entry)
textField.text = ""
}
tableView.reloadData()
}
Step 9 - Persist Data with UserDefaults
"When the data is added I would like it to be stored and not be deleted after the app is closed."
To save dataArray to UserDefaults, add this line of code after the line that appends an entry inside of the addButtonPressed(_:) action:
UserDefaults.standard.set(dataArray, forKey: "DataArray")
To load dataArray from UserDefaults, add these lines of code to viewDidLoad() after the call to super:
if let data = UserDefaults.standard.value(forKey: "DataArray") as? [String] {
dataArray = data
}
Try the following:
Create an array that will store all the text entered via the UITextField (ie. var array = [String]()
In the action of that add button, append the text the user has entered in the text field to the array.
if text.text != "" && !text.text.isEmpty {
// append the text to your array
array.append(text.text!)
text.text = "" // empty the `UITextField`
}
In your tableView methods, make the numberOfRows return array.count and just add a UILabel for your custom UITableViewCell that will display each entered item from the array in a separate cell.
if you want to display your data in tableview you need to implement tableview delegates. add a table view cell with a label on it
#IBOutlet weak var text: UITextField!
#IBOutlet weak var tableView: UITableView!
let NSUD_DATA = "dataarray_store"
var dataArray : NSMutableArray!
var userDefault = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
dataArray = NSMutableArray()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK:- create a button for adding the strings to array and while clicking that button
func onClickButton(){
let string = text.text
dataArray.add(string)
userDefault.set(dataArray, forKey: NSUD_DATA)
}
for getting array stored in userdefault
func getData() -> NSMutableArray?{
if userDefault.object(forKey: NSUD_DATA) != nil{
return userDefault.array(forKey: NSUD_DATA) as! NSMutableArray
}
return nil
}
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var entertxt: UITextField!
#IBOutlet weak var save: UIButton!
#IBOutlet weak var tableview: UITableView!
var names = [String]()
override func viewDidLoad() {
super.viewDidLoad()
if let data = UserDefaults.standard.object(forKey: "todolist") as?[String]
{
names = data
}
}
#IBAction func submit(_ sender: Any) {
if entertxt.text != "" {
names.append(entertxt.text!)
UserDefaults.standard.set(names, forKey: "todolist")
tableview.reloadData()
entertxt.text = ""
}
else
{
print("data not found")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! myTableViewCell
cell.namelable.text = names[indexPath.row]
return cell
}

How do I get the row of a custom UITableViewCell using a button in the custom cell, that will be sent to deleteRowsAtIndexPaths

I have made a table view in iOS that displays a list of buddy (friend) requests. For the buddy request cell, I have made it a prototype cell and have given it a custom class that extends from UITableViewCell. When I click the "Accept" button on the cell, I want to remove that row from the requests array I have and remove it from the table view as well.
The three options I have considered are
1) Giving the custom cell a property for row that corresponds to the row in the table, and hence, the row in the requests array. Then, when accept is called, pass that row to the delegate function and call
requests.removeAtIndex(row)
tableView.reloadData()
which updates all the custom cells' row property. This method works. However, is this a bad practice to reload the table data (it's only reloading from the stored array, not making a network request)
2) Giving the custom cell the row property, but then calling
self.requests.removeAtIndex(row)
self.requestsTableView.beginUpdates()
self.requestsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow:row, inSection: 0)], withRowAnimation: UITableViewRowAnimation.Fade)
self.requestsTableView.endUpdates()
However, this does not update the row value in each of the cells following the deleted cell, and I would somehow either have to update them all, or call reloadData() which isn't what I want to do.
3) Instead of passing the row value, when the "Accept" button is clicked, search for the username in the buddies list, get the index of where it is found, and then delete the row in the table using that index and deleteRowsAtIndexPaths. This seems okay to do, especially since I'll never have a huge amount of buddy requests at once and searching won't require much time at all, but I figure if I had immediate access to the row value, it would make things cleaner.
Here is the code:
View Controller
class RequestsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, RequestTableViewCellDelegate
{
// Outlet to our table view
#IBOutlet weak var requestsTableView: UITableView!
let buddyRequestCellIdentifier: String = "buddyRequestCell"
// List of buddies who have sent us friend requests
var requests = [Buddy]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
self.getBuddyRequests()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: -Table View
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return requests.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: RequestTableViewCell = tableView.dequeueReusableCellWithIdentifier(buddyRequestCellIdentifier) as! RequestTableViewCell
let buddy = requests[indexPath.row]
let fullName = "\(buddy.firstName) \(buddy.lastName)"
cell.titleLabel?.text = fullName
cell.buddyUsername = buddy.username
cell.row = indexPath.row
cell.delegate = self
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let buddy = self.requests[indexPath.row]
}
func didAccpetBuddyRequest(row: Int) {
// Remove buddy at the 'row' index
// idea 1: update all cells' 'row' value
//self.requests.removeAtIndex(row)
// reloading data will reload all the cells so they will all get a new row number
//self.requestsTableView.reloadData()
// idea 2
// Using row doesn't work here becuase these values don't get changed when other cells are added/deleted
self.requests.removeAtIndex(row)
self.requestsTableView.beginUpdates()
self.requestsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow:row, inSection: 0)], withRowAnimation: UITableViewRowAnimation.Fade)
self.requestsTableView.endUpdates()
// idea 3: don't use row, but search for the index by looking for the username
}
// MARK: -API
func getBuddyRequests() {
// self.requests = array of buddy requests from API request
self.requestsTableView.reloadData()
}
}
Custom UITableViewCell and protocol for the delegate call
protocol RequestTableViewCellDelegate {
func didAccpetBuddyRequest(row: Int)
}
class RequestTableViewCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var acceptButton: UIButton!
var delegate: RequestTableViewCellDelegate?
var buddyUsername: String?
var row: Int?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func touchAccept(sender: AnyObject) {
// <code goes here to make API request to accept the buddy request>
self.delegate?.didAccpetBuddyRequest(self.row!)
}
}
Thanks for taking the time to read this, I appreciate any help/best practices that you know that could help me in this situation.
There shouldn't be a problem with giving the cell the indexPath and delegate properties, and then informing the delegate when the Accept button has been tapped. You do need to call reloadData(), though, to update the references in the cells that are affected.
If you wish to minimise the number of reloaded rows, call reloadRowsAtIndexPaths() instead, but I think that creating the loop that creates the NSIndexPath objects will slow your app down just the same.
As an alternative I can suggest you another way:
First add action method to your acceptButton in viewController. Inside that method you can get indexPath of the cell that contains button. Here is implementation
#IBAction func acceptDidTap(sender: UIButton) {
let point = tableView.convertPoint(CGPoint.zeroPoint, fromView: button)
if let indexPath = tableView.indexPathForRowAtPoint(point) {
// here you got which cell's acceptButton triggered the action
}
}

Refresh Table Contents When View Reappears

I am writing a note taking app, just for reference. I have arrays set up, and a table that feeds off the arrays with the following code:
import UIKit
import Foundation
var tableData = ["Pancake Recipe", "Costume Party", "Camping Supplies"]
var tableSubtitle = ["Some Milk and some Flour", "Let's dress up like Jen", "Tenting with Lucy"]
class ViewController: UIViewController {
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier:"cell")
cell.textLabel!.text = tableData.reverse()[indexPath.row]
cell.detailTextLabel!.text = tableSubtitle.reverse()[indexPath.row]
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
var listTitle = "Notes"
self.title = listTitle
UIApplication.sharedApplication().statusBarStyle = .LightContent
}
override func viewDidAppear(animated: Bool) {
println(tableSubtitle)
}
}
A user creates a new title and subtitle for the cell on a different page, and these are added to the arrays (the tableData and tableSubtitle arrays). I know the adding of the data works correctly, because when I watch the console it prints both the updated arrays perfectly.
When I then return to the main view controller, I am presented with an extra cell (as I wanted) but instead of the new content that I want, it is instead just a duplicate of the 'Pancake Recipe' cell.
Do I need to refresh the content of the cells when the view loads again? If so, how can I do this?
Thanks :)
For reference, here is a picture of the Table View after data has been added to both the arrays twice, and I have then returned to the Table View, despite the fact both the arrays now contain two extra and distinct entries (checked using println(tableData) and println(tableSubtitle)
The provided code does not provide much information to find the issue, probably the issue will be with data adding code.
For refreshing the table view use:
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
self.yourTableView.reloadData()
}

DataSource appears to be working but tableView is not reloading

I've got an issue where my tableView isn't updating based on the datasource correctly. I'm using a tabbed application structure with Storyboards.
My overall goal here is to have a tableView on the second tab display items that are removed from an array stored in a struct. The items are added to the array from the first tab.
There are 2 ViewControllers (1 for the interface for scrolling through items and selecting to remove them, and 1 to handle the tableView) and 2 Views (1 for the interface for scrolling through items and removing them and 1 for the tableView). The first tab is for providing the interface for removing the items and the second tab is for the tableView.
The remove and add to the array functionality works, just not the displaying it in the tableView.
Currently, if I hard code items in my "removed items" array, they are displayed in the tableView. The problem is that as I add items to the array from my removeItem function in the first ViewController, the tableView does not update, only the hard coded items are shown.
This makes me assume that I have my datasource and delegate setup correctly, since the tableView is getting it's data from the intended datasource. The issue is it's not updating as the user updates the array with new items.
I've tried using self.tableView.reloadData() with no success. I might not be calling in the correct location though.
I'm not sure where the disconnect is.
Here is my second view controller that controls the tableView
class SecondViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let cellIdentifier = "cellIdentifier"
var removedTopicsFromList = containerForRemovedTopics()
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView?.registerClass(UITableViewCell.self, forCellReuseIdentifier: self.cellIdentifier)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// UITableViewDataSource methods
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return removedTopicsFromList.removedTopics.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier) as UITableViewCell
cell.textLabel!.text = self.removedTopicsFromList.removedTopics[indexPath.row]
return cell
}
Here is the struct where the removed phrases are stored
struct containerForRemovedTopics {
var removedTopics: [String] = []
}
structure instances are always passed by value. So if your code is something like:
var removedTopicsFromList = secondViewController.removedTopicsFromList
removedTopicsFromList.removedTopics.append("SomeTopic")
secondViewController.reloadData()
then you are changing the different structure.
Maybe you got stuck with this problem I guess.

Resources