UISearchController retain issue - ios

I am trying to use UISearchController however I confronted with retain issue that I can't solve. MainTableview has two sections.
Section 1
Filtered Data based on some Regex
Section 2
All Data
I added UISearchController to my tableview and attached ResultsTableController as resultsTableController. It works when user search something, ResultsTableController comes forward and because I set tableview delegate to self, selecting item from ResultsTableController calls didSelectRowAtIndexPath in my MainTableViewController. However I have allocation issue if user selects something from resultsTableController.
Following happens for different scenarios
User doesn't search anything, just selects an item from
MainTableview, I see deinit messages
User searches something, cancel the search, select item from
MainTableview, I see deinit messages
User searches something, and selects an item from
ResultsTableController, I don't get deinit in my viewcontrollers
MainTableViewController.swift
var searchController: UISearchController!
// Secondary search results table view.
var resultsTableController: ResultsTableController!
var allCompanies = ["Data1","Data2","Data3"]
override func viewDidLoad() {
super.viewDidLoad()
resultsTableController = ResultsTableController()
// We want to be the delegate for our filtered table so didSelectRowAtIndexPath(_:) is called for both tables.
resultsTableController.tableView.delegate = self
searchController = UISearchController(searchResultsController: resultsTableController)
searchController.searchResultsUpdater = self
searchController.searchBar.sizeToFit()
tableView.tableHeaderView = searchController.searchBar
searchController.delegate = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.delegate = self
definesPresentationContext = true
}
}
// MARK: UISearchBarDelegate
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
// MARK: UISearchResultsUpdating
func updateSearchResultsForSearchController(searchController: UISearchController) {
// Update the filtered array based on the search text.
let filteredResults = allCompanies.filter({ company in
(company.lowercaseString as NSString).containsString(searchController.searchBar.text.lowercaseString)
})
// Hand over the filtered results to our search results table.
let resultsController = searchController.searchResultsController as! ResultsTableController
resultsController.searchResult = filteredResults
resultsController.tableView.reloadData()
}
// usual tableview methods
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if resultsTableController.searchResult.count > 0 {
selectedCompany = resultsTableController.searchResult[index]
//do something with selected company
navigationController?.popViewControllerAnimated(true)
return
}
//
selectedCompany = allCompanies[index]
navigationController?.popViewControllerAnimated(true)
}
deinit {
println("MainTableView deinit")
}
ResultTableController.swift
class ResultsTableController:UITableViewController {
var searchResult = [String]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResult.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! UITableViewCell
let index = indexPath.row
cell.textLabel?.font = UIFont(name: "Avenir-Roman", size: 16)
cell.textLabel?.text = searchResult[index].description
return cell
}
deinit {
println("ResultTableController deinit")
}
}

Hey there I ran into the issue today
apparently I need to force the dismiss of the searchController to work around the retain issue
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
searchController?.dismissViewControllerAnimated(false, completion: nil)
}
here is my sample project
https://www.dropbox.com/s/zzs0m4n9maxd2u5/TestSearch.zip?dl=0

The solution does seem to be to call dismissViewControllerAnimated on the UISearchController at some point. Most people probably don't do that since the UISearchController is somewhat of an implementation detail related to your view controller that is hosting the UISearchController.
My solution, which seems to work no matter how you present your search UI (standard present or in a popover) is to call searchController.dismissViewControllerAnimated() from your host's viewDidDisappear, after checking to see if the view controller is no longer being presented. This catches all cases, especially the popover case where the user taps outside the popover to automatically dismiss the UI, or the case where the search UI is disappearing simply because you pushed something else onto the navigation stack. In the latter case, you don't want to dismiss the UISearchController.
override func viewDidDisappear(animated: Bool)
{
super.viewDidDisappear(animated)
if presentingViewController == nil
{
searchController.dismissViewControllerAnimated(false, completion: nil)
}
}

Related

How to replace the content on screen with a tableview when UISearchBar is clicked

I know there's been questions like this before: here or here
But it hasn't gone into much detail, as much as I'd like anyway. I have this 'Explore' page screen sort of thing. It has a search bar at the top and I'd like to show content/posts in the middle section of it. But when the user clicks on the search bar, I want go to a tableview controller, replacing the content on the screen, to show the search results.
I had a tableviewcontroller embedded in the UIView container:
import UIKit
import Firebase
class SearchTableViewController: UITableViewController, UISearchResultsUpdating {
let searchController = UISearchController(searchResultsController: nil)
#IBOutlet var searchResultsTableView: UITableView!
var usersArray = [NSDictionary?]()
var filteredIsers = [NSDictionary?]()
var ref = Database.database().reference()
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
ref.child("users").child("public").queryOrdered(byChild: "username").observe(.childAdded, with: { (snapshot) in
if let user = snapshot.value as? [String:Any]{
let username = user["username"] as? String ?? ""
if(username == SpalshScreenViewController.UserData.username){
return
}else{
self.usersArray.append(snapshot.value as? NSDictionary)
}
}else{
return
}
print(self.usersArray)
// insert rows
self.searchResultsTableView.insertRows(at: [IndexPath(row: self.usersArray.count-1, section: 0)], with: UITableView.RowAnimation.automatic )
})
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
if searchController.isActive && searchController.searchBar.text != "" {
return filteredIsers.count
}else{
return self.usersArray.count
}
}
func updateSearchResults(for searchController: UISearchController) {
filterContent(searchText: self.searchController.searchBar.text!)
}
func filterContent(searchText: String){
self.filteredIsers = self.usersArray.filter{ user in
let username = user!["username"] as? String
return(username?.lowercased().contains(searchText.lowercased()))!
}
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let user : NSDictionary?
if searchController.isActive && searchController.searchBar.text != "" {
user = filteredIsers[indexPath.row]
}else{
user = self.usersArray[indexPath.row]
}
cell.textLabel?.text = user?["firstName"] as? String
cell.detailTextLabel?.text = user?["username"] as? String
let profilePicUrl = user?["profilePicURL"] as? String
let encodedurl = URL(string: profilePicUrl!)
print(profilePicUrl)
cell.imageView!.kf.setImage(with: encodedurl, placeholder: UIImage(named: "profile_pic"))
return cell
}
}
Would I have that code in it's separate UITableViewController and then present that view when the search bar is clicked or?
I know you'd change the let searchController = UISearchController(searchResultsController: nil) to something like let searchController = UISearchController(searchResultsController: tableViewController)
but I don't know how to tie everything in. In my searchViewController I have reference to the UISearchBar, I was thinking I'd add a tap gesture onto it and present a the tablecontrollerview when it's clicked but that would mean there would be an inconsistency in animation and search bar style?
EDIT:
So basically, I have a view controller, in that view controller I have a search bar at the top (doing nothing at the moment) and underneath that search bar will be a collection view full of different posts. When the user clicks on the search bar I'd like to replace that collection view with a table view with the results of the search result. How would I achieve that?
You should set up searchResultsController and searchResultsUpdater properties of the UISearchController class
Concerning UISearchBar object, you have to use the one which comes with UISearchController and add it programmatically to a view.
view.addSubview(searchController.searchBar)
Hey #nathan if you can please explain in more details than it would be better. Other wise on base of my current understanding. If you have search bar added in top of view and you have the table view in it also then you can update your table view in search bar delegate methods. I am sharing the search bar delegate methods for your help.
extension SearchViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
let str = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
viewModel?.search(text: str)
tableView.reloadData()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if let t = searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines) {
if t.count > 0 {
viewModel?.addNewSearch(text: t)
tableView.reloadData()
}
}
searchBar.resignFirstResponder()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
viewModel?.cancel()
navigationController?.popViewController(animated: true)
} }
I have used MVVM in my code so ViewModel is basically used for data update and changes. Thanks you can ask if you have any confusion.

Navbar title become small when scroll down and go back from a UITableViewController

The main navigation bar become small when go back from a table view with scrolling. Can anyone show me the correct way to implement large title?
Video Sample
https://i.imgur.com/zoATpja.gif
ViewController
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationController?.navigationBar.prefersLargeTitles = true
}
DestinationViewController
let reuseIdentifier = "cell"
let array = ["Test 1","Test 2","Test 3"]
override func viewDidLoad() {
self.title = "TableView"
self.navigationItem.largeTitleDisplayMode = .never
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier)
self.tableView.delegate = self
self.tableView.dataSource = self
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
cell.textLabel?.text = array[indexPath.row]
return cell
}
This helps you!!
Call this method from viewDidLoad()
/**
In Swift 4.2
*/
func setupNavBar() {
self.title = "titleName"
self.navigationController?.navigationBar.prefersLargeTitles = true
self.navigationController?.navigationItem.largeTitleDisplayMode = .always
}
In DestinationView Controller put this two lines in ViewDidLoad method.
self.navigationItem.largeTitleDisplayMode = .never
self.navigationController?.navigationBar.prefersLargeTitles = false
Well, after tinkering with the issue, I've come to a conclusion that:
This is probably a bug when using Large titles in combination with UIViewController.
Then I found in one of your comments: but iPhone Settings and App Store has the similar animation...
But the thing is that both of the reference apps uses UITableViewController subclass for the source (of the segue) view controller when the navigation happens. So I tried similar approach and YES my doubt is correct. You can find the reference project here where the animation issue isn't present.
So, you might want to change your source view controller to be a subclass of UITableViewController until there is an official fix for the issue.
You need to call this
self.navigationController?.navigationBar.prefersLargeTitles = true
and this
self.navigationItem.largeTitleDisplayMode = .never
in viewWillAppear()

tableView.hidden breaks other elements

I have in my iOS app a mapView, and a UISearchBar to search locations and center the user on it. The results of my search are made by autocompletion and displayed in a tableView.
I had trouble with it, when i clicked on cancel, it displayed me all of my datas in the tableView.
So I implemented this function:
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
searchTableView.hidden = true
}
But the render of that function makes this:
I don't know (and don't know how to see) if my mapView is still under the black screen.
By the way, if I delete my cancel button function, and search something, if I click on Cancel it will display something like this:
To hide my tableView again, I have to make another search, than delete it with the little cross button, then cancel.
My code, of the autocompletion search, is like that:
func initTableView() {
searchTableView.delegate = self
searchTableView.dataSource = self
searchTableView.scrollEnabled = true
searchTableView.hidden = true
}
func searchBarSearchButtonClicked() {
searchTableView.hidden = false
searchBar.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
searchBar(searchBar, textDidChange: searchBar.text!)
}
func searchBar(_searchBar: UISearchBar, textDidChange searchText: String) {
searchTableView.reloadData()
searchAutocompleteEntriesWithSubstring(searchText)
}
func searchAutocompleteEntriesWithSubstring(substring: String) {
searchBar.filtredDatas.removeAll(keepCapacity: false)
for curString in searchBar.datas {
let myString:NSString! = curString.title! as NSString
let substringRange :NSRange! = myString.rangeOfString(substring)
if (substringRange.location == 0) {
searchBar.filtredDatas.append(curString)
}
}
}
This is from my ViewController.swift file.
My searchBar variable is a custom class that contains an NSArray of datas and another of fileterDatas.
#IBOutlet weak var mapView: UGOMapController!
#IBOutlet var searchTableView: UITableView!
#IBOutlet weak var searchBar: UGOSearchBar!
Thanks for your help.
If you need more of my code, I can edit this post.
Edit:
Here are my UITableView functions, used for searchTableView
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchBar.filtredDatas.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let autoCompleteRowIdentifier = "AutoComplete"
var cell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier) as UITableViewCell!
if cell != nil {
let index = indexPath.row as Int
if (searchBar.filtredDatas.count > 0) {
cell!.textLabel!.text = searchBar.filtredDatas[index].title
}
}
else {
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: autoCompleteRowIdentifier)
}
return cell!
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedCell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
searchBar.text = selectedCell.textLabel!.text
}
And my hierarchy:
And my viewController inspector:
You have 3 different outlets connected to the table view.
It means that when you hide the tableView, you also hide the view, because it's connected to the same Search Table View
You have to make sure that you connect your table view just to one outlet.
Edit
To be more precise:
You have to change your view controller from UITableViewController to UIViewController.
The view outlet will be connected to viewController's view property.
The tableView outlet will be removed, because in a normal UIViewController this property doesn't exist. (It's defined in UITableViewController)
SearchTableView should be connected to the searchTableView property that you define in your view controller.

IOS: Scrollable search bar similar to Mail or Messages apps

New iOS developer here! I love the design of scrolling up to a hidden search bar. I want to duplicate it but a lot of the solutions seem outdated, so i am really confused.
iPhone: Hide UITableView search bar by default
Going off of this, it seems like there's 2 steps.
Add search bar to the scrollable table view
Add code so it scrolls to the second row on app load (in viewdidload?) using
yourTableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: false)
I can't get passed #1. I tried putting in a search bar in 3 spots...
A) If I put it here (on top of the header?) it sticks at the top as expected, but not scrollable.
B) If I put it here, it's still not scrollable and it blocks the first row
C) If I put it here, it repeats on the row, as expected.
Somehow I need to put it on the table without duplicating, so I'm confused on that.
So I think you have to do this in code. If you want to do it, I followed the tutorial here and it worked.
http://www.ioscreator.com/tutorials/add-search-table-view-tutorial-ios8-swift
Add UISearchResultsUpdating
class TableViewController: UITableViewController, UISearchResultsUpdating {
Add these properties...
let tableData = ["One","Two","Three","Twenty-One", "vasdf", "vasdfd", "3343", "ad23", "454d", "vasdf32", "va343", "vasdf3r2", "vasdfd2", "vasr52f", "awefwr32vwf"]
var filteredTableData = [String]()
var resultSearchController = UISearchController()
Add self.resultSearchController here...
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
})()
// Reload the table
self.tableView.reloadData()
}
Take care of self.resultSearchController.active conditions in these functions...
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// 2
if (self.resultSearchController.active) {
return self.filteredTableData.count
}
else {
return self.tableData.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
// 3
if (self.resultSearchController.active) {
cell.textLabel?.text = filteredTableData[indexPath.row]
return cell
}
else {
cell.textLabel?.text = tableData[indexPath.row]
return cell
}
}
Then do the search code here
func updateSearchResultsForSearchController(searchController: UISearchController)
{
filteredTableData.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text as String!)
let array = (tableData as NSArray).filteredArrayUsingPredicate(searchPredicate)
filteredTableData = array as! [String]
self.tableView.reloadData()
}
And I think that's it. Good luck!

Swift – Table view data not reloading after dismissing view controller

I have a view in my app called JournalViewController that I'm presenting over my PastSessionsViewController. PastSessions has a table view that the user can tap to edit and bring up the journal.
When the user edits an entry and saves it (saving to CoreData), dismissing JournalViewController I'd like for the table view in PastSessions to reflect those changes and show the updated table cell.
I'm calling tableView.reloadData() in PastSessionsViewController viewDidLoad() but that doesn't seem to be working. I've also added a delegate for JournalViewController to interact with PastSessionsViewController ahead of dismissViewController
Here's some code to look at:
In PastSessionsViewController:
class PastSessionsViewController: UIViewController, UITableViewDelegate, JournalVCDelegate {
weak var tableView: UITableView?
weak var backButton: UIButton?
let pastSessionsDataSource: PastSessionsDataSource
init() {
pastSessionsDataSource = PastSessionsDataSource()
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let tableView = UITableView()
tableView.backgroundColor = nil
tableView.delegate = self
tableView.dataSource = pastSessionsDataSource
tableView.registerClass(EntryCell.self, forCellReuseIdentifier: "cell")
view.addSubview(tableView)
self.tableView = tableView
}
override func viewDidAppear(animated: Bool) {
tableView?.reloadData()
}
func didFinishJournalVC(controller: JournalViewController) {
var newDataSource = PastSessionsDataSource()
tableView?.dataSource = newDataSource
// tried this ^, but it's causing the app to crash
// tableView?.reloadData() <- this isn't doing the trick either
dismissViewControllerAnimated(true, completion: nil)
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let editJournalVC = JournalViewController(label: "Edit your thoughts")
editJournalVC.delegate = self
presentViewController(editJournalVC, animated: true, completion: nil)
}
}
In JournalViewController:
protocol JournalVCDelegate {
func didFinishJournalVC(controller: JournalViewController)
}
class JournalViewController: UIViewController, UITextViewDelegate {
var delegate: JournalVCDelegate! = nil
func doneJournalEntry(sender: UIButton) {
journalEntryTextArea?.resignFirstResponder()
... do some core data saving ...
delegate.didFinishJournalVC(self)
}
}
In PastSessionsDataSource:
import UIKit
import CoreData
class PastSessionsDataSource: NSObject {
var arrayOfEntries = [Entry]()
var coreDataReturn: [Meditation]?
func prepareEntries() {
// gets stuff from coredata and formats it appropriately
}
override init() {
super.init()
prepareEntries()
}
}
extension PastSessionsDataSource: UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayOfEntries.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! EntryCell
... set up the labels in the cell ...
return cell
}
}
Thanks for looking!
viewDidLoad is called when the view controller load its view at the first time, so basically it will only be called once during the view controller's whole life cycle.
One quick solution is to put tableView.reloadData() in PastSessionsViewController viewWillAppear() or viewDidAppear().
However I do not like this quick solution as every time you dismiss JournalViewController, the table view will be reloaded, even the user has not changed anything on JournalViewController (for example, cancel the edit). So I suggest to use delegate approach between PastSessionsViewController and JournalViewController, when the user actually edit the data on JournalViewController then inform PastSessionsViewController to refresh the table.
You are currently prepare entries only on init of PastSessionsDataSource, but not after you did CoreData changes. So each time when you reloadData for tableView you work with the same data set loaded initially. As a quick hack you can try to updated viewDidAppear in a following way:
override func viewDidAppear(animated: Bool) {
if let tableView = tableView {
let dataSource = tableView.dataSource! as PastSessionsDataSource
dataSource.prepareEntries()
tableView.reloadData()
}
}
Your tableView property is probably nil in viewDidAppear, based on your listed code. The reason is that in viewDidLoad you construct a UITableView as tableView, and that is a local variable. You need to assign that variable to the property:
self.tableView = tableView

Resources