Problem:
I have a table view that the user can either scroll through to find something or use a search bar. The search bar was not created using the StoryBoard. My view has a UISearchController that handles the search bar and search result updating. The issue that I'm running into is that since my SearchResultsController is instantiated by another controller I cannot perform a segue to another view, or at least I can't find the solution. Everything works except for the segue between my Search Results Controller and the view it's destined for.
What I would like to do
Have my MyNavTableViewController delegate for MySearchResultsController. In the search results controller it will segue to another view when the user taps on a TableViewCell. I'm not using the StoryBoard to accomplish this I'm having difficulty using segues to transition to another view.
If I can't get this to work what I will probably do:
It's essential that I pass information between views, and for me I've always done it using segues. However if this doesn't work I will probably try presenting a view modally by pushing it unto the navigation controller stack and write the data to a shared database or something. I would prefer to use a segue though.
Research:
There is definitely more than this, but I'm not going to take too much space on urls.
Creating a segue programmatically
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html
My Setup
I'm going to try and keep this as concise as possible. There is more code than what I'm displaying. I'm just going to try and clean it up so that I'm only presenting the important stuff. I'm also changing a few names around because there may be sensitive information. It's probably not a big deal, but I'd rather be safe.
class MyNavTableViewController: UIViewController, UITableViewDataSource{
//this is
#IBOutlet weak var tableView: UITableView!
var searchController: UISearchController!
override func viewDidLoad(){
...code
tableView.registerClass(UITableViewCell.self,forCellReuseIdentifier: tblId)
let resultsController = MySearchResultsController()
resultsController.databaseFilePath = databaseFilePath()
//this is essential that I use a segue because between my views I'm passing information between them.
resultsController.photo = photo!
searchController = UISearchController(searchResultsController: resultsController)
let searchBar = searchController.searchBar
searchBar.placeholder = searchBarPlaceHolderText
searchBar.sizeToFit()
tableView.tableHeaderView = searchBar
searchController.searchResultsUpdater = resultsController
}
}
MySearchResultsController: UITableViewController, UISearchResultsUpdating {
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
//self.performSegueWithIdentifier(imagePrepareStoryBoardId, sender: tableView.cellForRowAtIndexPath(indexPath))
/*let imagePrepare = ImagePrepareController()
customSegue = SearchResultSegue(identifier: imagePrepareId, source: self, destination: imagePrepare)*/
//neither storyboard nor navigationController can be nil.
let destVC = self.storyboard!.instantiateViewControllerWithIdentifier(imagePrepareStoryBoardId)
//destVC.photo = photo!
//self.presentViewController(destVC, animated: false, completion: nil)
self.navigationController!.pushViewController(destVC, animated: false)
}
}
My Failed Attempts
1) Straight up segue - Doesn't work since the MySearchResultsController is not a view in the storyboard. Everything from what I've read is that segues can only be created in the SB.
2) Push view onto the navigation stack. The only problem with this is that I can't send data between views (or at least from what I've read). I'm also getting this error right at this break point:
let destVC = self.storyboard!.instantiateViewControllerWithIdentifier(imagePrepareStoryBoardId)
fatal error: unexpectedly found nil while unwrapping an Optional value
I double checked the imagePrepareStoryBoardId. It's correct.
3) Use custom segue - As you can see from the commented out lines that this isn't working either. I've always used segues with the SB so this method is a little new to me. I might be messing it up somewhere.
First create a protocol
protocol SelectedCellProtocol {
func didSelectedCell(text: String)
}
on your UITableViewClass declare it
class MySearchResultsController: UITableViewController, UISearchResultsUpdating {
var delegate:SelectedCellProtocol?
}
and on the selected cell method call it like :
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
self.delegate?.didSelectedCell(cell.textLabel?.text)
}
when you declare your results controller, set the delegate
let resultsController = MySearchResultsController()
resultsController.databaseFilePath = databaseFilePath()
//this is essential that I use a segue because between my views I'm passing information between them.
resultsController.photo = photo!
resultsController.delegate = self
searchController = UISearchController(searchResultsController: resultsController)
let searchBar = searchController.searchBar
searchBar.placeholder = searchBarPlaceHolderText
searchBar.sizeToFit()
tableView.tableHeaderView = searchBar
searchController.searchResultsUpdater = resultsController
and then implement the protocol
class MyNavTableViewController: UIViewController, UITableViewDataSource, SelectedCellProtocol{
func didSelectedCell(text: String) {
print(text)
//make your custom segue
}
}
Related
I encountered this bug where my previous view in the NavigationController hierarchy incorrectly shows up briefly before my newly pushed view is loaded. I couldn't diagnose or reproduce reliably. Here are some details.
The Complete NavigationController sequence is this:
A(ViewController w/ SearchController)
-> B(ViewController as SearchResultController, only shows up for search result)
-> C1(ViewController for the specific record returned from search, pushed via a delegate method of B that uses the NavigationController
in A to push to C, so note that B does not have navigation controller,
but B stays in the view under the first C and will show when returned
from C)
-> C2(pushed a new C by itself)
Expectation is, when C1 pushes the C2, only C2 is shown
But the actual behavior seen here is, when C1 pushes C2, B shows up for a split second before abruptly becoming C2. But nowhere is this specified.
I did the following things to pinpoint the issue as this only occurs once in about 40-50 random searches and has no pattern to find (the same search results won't produce the behavior the second time), but once observed, this behavior will stick on that specific view of C1 and will reproduce indefinitely when C2 is pushed, it will however return to normal when view is popped back to B from C1 and and the same C1 would not produce this behavior anymore when clicked again.
I checked console output, no error or warning generated.
I added breakpoint before the new C is pushed and checked if the data needed for initializing new C properly is there and it was there.
I added breakpoints in ViewDidLoad, ViewWillAppear and ViewDidAppear, and found that B showed up after ViewWillAppear and stayed til ViewDidLoad, only after ViewDidLoad was finished did new C show up abruptly. I am still confused as apparently the new C is not "loaded" when the viewdidload executed.
I can't find any related information to debug this. The platform is iOS16 and I am using UIKit
Below are the minimal representation of my codes
In View A:
class SearchViewController: UIViewController, UISearchResultsUpdating,{
let searchController : UISearchController = {
let vc = UISearchController(searchResultsController: SearchResultsViewController())
vc.definesPresentationContext = true
return vc
}()
override func viewDidLoad(){
searchController.searchResultsUpdater = self
}
func updateSearchResults(for searchController: UISearchController){
let result = [] //basically any array of results object
guard let resultsController = self.searchController.searchResultsController as? SearchResultsViewController,
let query = searchController.searchBar.text, !query.isEmpty else{
return
}
resultsController.delegate = self
resultsController.update(with:result)
}
}
//extension for the delegate of view B
extension SearchViewController : SearchResultsViewControllerDelegate{
func didTapResult(_ result : Record){
let vc = RecordDetailViewController(rec: result)
navigationController?.pushViewController(vc, animated: true)
}
}
In View B:
class SearchResultsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
weak var delegate : SearchResultsViewControllerDelegate?
private let tableView : UITableView = {
let tableView = UITableView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "myCell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
//viewAllFonts()
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.keyboardDismissMode = .onDrag
} //viewDidLoad
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
func update(with results: [Record]){
filteredData = results
tableView.reloadData()
}
//omitting othertableview methods
//this is the part that delegates the didtapresult to view A
//has to do this bc navigationcontroller in this class is nil
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//print(filteredData[indexPath.row])
tableView.deselectRow(at: indexPath, animated: true)
delegate?.didTapResult(filteredData[indexPath.row])
}
}
in View C:
class RecordDetailViewController: UIViewController{
//omitting tableview var closure, same as view B basically
init(rec : Record){
self.rec = rec
super.init(nibName: nil, bundle: nil)
print("alloced")
}
required init?(coder: NSCoder) {
fatalError()
}
deinit {
print("dealloced")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.largeTitleDisplayMode = .never
}
//omiting configure method
override func viewDidLoad() {
super.viewDidLoad()
configure()
self.view.addSubview(self.tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.frame = view.bounds
}
}
extension RecordDetailViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let rec = Record()
//when I added breakpoint here, rec is properly initialized before vc is pushed
//as you can see this is pushing another view of this class
//after briefly pushing this, view B table appears for brief second.
let vc = RecordDetailViewController(rec: rec!)
self.navigationController?.pushViewController(vc, animated: true)
}
}
Following #Jacob Sheldon advice, I added debug statement in view C's init and deinit method. It appears that all view controllers are allocated and deallocated normally. There are no retaining views. Basically, each alloc prints comes with a dealloc print.
I also tried to capture UIHierarchy in Xcode, I first try to produce the behavior by clicking random results(B) and click table row(C1), then when the behavior is produced in C1->C2, I set up the Xcode in C1 and clicked "View UI Hierarchy" when the transition between c1->c2 just showed view B. This however did not capture the UIHierarchy of view B but still that of view C2, which is rendered correctly according to UIViewHierarchy.
I checked profiler and observed the memory usage and allocation usage. Nothing is out of line, the memory did go up and down with database results searches but it always returned to normal (~30 MB)when returning to view A.
Edit
After weeks of trying different things, I still couldn't even replicate or eliminate this bug reliably. It always happens in random. I made sure the init value for the view C is not null. There is no memory leak, all navigation controllers are correctly set up. I also moved all my code out of storyboards so now every behavior is done programmatically. One thing I noticed is that this bug never appears on the iOS 16 simulator, but does appear on my real iPhone 12 running iOS16.
I suspected if this is because of dispatch queue issue where the data is not available when the view is pushed, but the data is there, and the weird thing is all subsequent pushes to view D (a completely different and static view controller) are bugged too, this couldn't possibly be the data issue.
This is behavior in question
this is expected behavior
Due to no source code, I just can give you one clue. Maybe it can solve your issue, maybe not.
The most possible reason is the vc isn't be released normally. you can add the dealloc (for Objc) or the deinit (for Swift) method in your b and c vc implementation.
If you reallise the vc is not be released as expected, you just need to check is there retain cycle in vc code. Especially the callbacks of network or subviews.
If you can give more about your code or pseudocode, the issue can be solved more quickly.
I have an UITableView which works perfectly fine. It is nested in a UITabbarController.
I also integrated an UISearchController with a UISearchBar, which lets the user search the content of the UITableView. This also works fine.
When you select one of the TableViewCells, you get to a DetailView.
When you do this without active search, everything works fine, but when you enter a searchterm and you choose a cell from the results, there is no way to get back to the TableView as there is no back button in the top left corner, neither can you use the swipe back gesture.
I may have to add, that I do not specify a separate UITableView for the searchresults:
self.searchController.searchBar.delegate = self
self.searchController.searchResultsUpdater = self
self.tableView.tableHeaderView = self.searchController.searchBar
self.definesPresentationContext = true
I define the SearchController like this:
var searchController: UISearchController = {
let controller = UISearchController(searchResultsController: nil)
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.sizeToFit()
return controller
}()
I thought, that maybe I have to put a UINavigationController into the UITabbarController and set my TableView as its RootViewController, but then I had the problem, that the SearchBar was hidden behind the NavigationBar...
EDIT (#ryantxr):
my didSelectRowAtIndexPath:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
in cellForRowAtIndexPath there is no difference between search and no search as the cells look the same. This is also the reason, why I don't use two different segue yet.
But you are right, I don't use different segue when using search and no search. The only segue used when a cell is clicked is setup in the storyboard and in prepareForSegue I set the attributes of the target UIViewController, which also works fine.
Turns out that the problem was this little piece of code in the UITableViewController which contains the UISearchController was responsible for this:
self.definesPresentationContext = true
by deleting this, the result view controller was shown correctly and the navigationbar did not disappear so it was possible to go back to the searchresults ect...
A portion of my app has an embedded master-detail section. Each detail view is using a custom UIViewController. When I change the value of something inside one of these UIViewControllers I need to be able to grey out one of the table rows in the master UITableViewController.
The closest I have seen to a solution is to use NSNotificationCenter to bubble up any changes, though this feels a little untidy..
Another solution is to use delegates? But I haven't come across any example solutions or tutorials in how to use this in Swift?
I've also experimented just trying to access the table view by navigating back up the hierarchy:
let navController = self.splitViewController!.viewControllers[0];
navController.tableView.reloadData()
I know the example above is wrong, but I don't know how to access the master view that way, or even if it is the right approach.
Oh, I am trying to call reloadData() because in the master view there is some logic which checks the condition as to wether to grey out a table row is applicable (i'm using Core Data)
I've seen that you figured this one out already. However a cleaner and more future proof way would be to use a delegate protocol:
protocol DetailViewControllerDelegate: class {
func reloadTableView()
}
Then add a delegate property to your DetailViewController class and implement the call to the delegate:
class DetailViewController: UIViewController {
weak var delegate: DetailViewControllerDelegate?
....
func reloadMasterTableView() {
delegate?.reloadTableView()
}
}
And then in your MainViewController implement the delegate method:
extension MainViewController: DetailViewControllerDelegate {
func reloadTableView() {
tableView.reloadData()
}
}
Don't forget to set the delegate on your DetailViewController instances when you create them:
let detailViewController = DetailViewController()
detailViewController.delegate = self
I would suggest you use NSNotificationCenter .
If you want to to do it via Navigation controller here is to code should work for you in swift.
let navController: UINavigationController = self.splitViewController!.viewControllers[0] as! UINavigationController
let controller: MasterViewController = navController.topViewController as! MasterViewController
controller.tableView.reloadData()
Since I was able to access my viewController, I was able to access the parent viewcontroller like so:
func reloadMasterTableView(){
let navVC: UINavigationController = self.splitViewController!.viewControllers[0] as! UINavigationController
let sectionsVC : UIMasterViewController = navVC.topViewController as! UIMasterViewController
sectionsVC.tableView.reloadData()
}
I have a problem with a UISearchBar. When ill search some Text in combination with an UITableView, and ill click on one result Cell, the UISearchBar is still visible in the next View Controller. If ill go back (with Segues) - the UISearchbar is still there (with the Keyword)
So after ill click on one result, ill get (in the next View Controller):
Ill use it this way:
class ...: UITableViewController, UISearchResultsUpdating {
var filterSearchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
filterSearchController.searchResultsUpdater = self
filterSearchController.hidesNavigationBarDuringPresentation = false
filterSearchController.dimsBackgroundDuringPresentation = false
filterSearchController.searchBar.searchBarStyle = .Minimal
filterSearchController.searchBar.sizeToFit()
self.tableView.tableHeaderView = filterSearchController.searchBar
Any ideas what could be a problem?
You need to dismiss the UISearchController yourself before transitioning to the next view controller with:
filterSearchController.active = false
I'm trying to wrap my head around protocols and delegates, but seems to be having some issues. I have 2 ViewControllers that I'm trying to pass data from. ViewController A has a text field that I want to be optionally populated from ViewController B. So there is a button on ViewController A that segues you over to ViewController B This is how I have B set up.
protocol AddPlayersViewControllerDelegate{
var playersName:String? { set get }
}
class B-Controller: UIViewController, UITableViewDelegate, UITableViewDataSource{
var addPlayerDelegate:AddPlayersViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
..etc
}
I'm using this code in my viewControllers B class to dismiss the currentView when a cell is selected
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
println("did select")
let cell = playerTableView.cellForRowAtIndexPath(indexPath)
addPlayerDelegate?.playersName? = "New Name"
if let navController = self.navigationController {
navController.popViewControllerAnimated(true)
}
}
It's not allowing me to set the players Name property inside the protocol here. It keeps returning me Nil when I check it from ViewController A.
View Controller A looks like this:
class A-ViewController: UIViewController, UITextFieldDelegate, AddPlayersViewControllerDelegate{
var addPlayerDelegate:AddPlayersViewControllerDelegate?
}
//then I'm just trying to print out the new name the was set in ViewController B
override func viewWillAppear(animated: Bool) {
println("this is the selected players name \(addPlayerDelegate?.playersName)") - returns nil
}
I'm sure I'm not fully understanding something, but I feel that I just keep reading and trying out examples only to end back up here where I started from.
//************************* UPDATE *************************//
I'm going to try and simplify my set up. I have 2 View Controllers, VC-A, and VC-B.
VC-A has a text field and a button. VC-B has a tableview. I want the option to have the textField to be populated from the cell.text from CB-B, but only if the user taps the button to view VC-B. So the first time that VC-A loads, it should being back nil from my playersName string from the protocol, because VC-B has never been called as of yet. But once the user taps the button inside VC-A to view VB-B and then selected a cell, which would dismiss VC-B and populate the playersName string inside the protocol on the VC-B class, then I'm using the viewWillAppear method to check to see if playersName has been set and if so use it. Here is my updated code from the help you have given me.
VC-A
class FirstViewController: AddPlayersViewControllerDelegate{
var playersName:String?
let svc = LookUpViewController()
override func viewDidLoad() {
super.viewDidLoad()
svc.addPlayerDelegate = self
}
}
VC-B
protocol AddPlayersViewControllerDelegate{
var playersName:String? { set get }
}
class LookUpViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var addPlayerDelegate: AddPlayersViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = playerTableView.cellForRowAtIndexPath(indexPath)
addPlayerDelegate?.playersName = "Ziggy"
println("the name to be pass is \(addPlayerDelegate?.playersName)")
if let navController = self.navigationController {
navController.popViewControllerAnimated(true)
}
}
It seems that I'm still getting nil even when I got back to VC-A from VC-B. All I want is to be able to get some data (string) from VC-B and use it in VC-A, but only after the user uses the VC-B class. Does that make sense?
You have shown that in the BController you have a property addPlayerDelegate:
var addPlayerDelegate:AddPlayersViewControllerDelegate?
And you have shown that in the BController you talk to that property:
addPlayerDelegate?.playersName? = "New Name"
But you have not shown that at any point in the lifetime of this controller, its addPlayerDelegate property is ever set to anything. For example I would want to see code like this:
someBController.addPlayerDelegate = someAController
If that doesn't happen, then that property remains at its initial value of nil.
Another problem with your code is that this line makes no sense:
class A-ViewController : // {
var addPlayerDelegate:AddPlayersViewControllerDelegate?
}
The AController doesn't need this property. They don't both need delegates! What the AController needs is a playersName property. Without it, it doesn't conform to the AddPlayersViewControllerDelegate protocol. In fact, I'm surprised that without that property your code even compiles. Are you sure you are reporting it correctly?