I have UITableView with two static cells. Each cell has custom class and independently validate account name, when I fill text field in the cell. (This part of code I got as is and I am not allowed to rewrite it). The cell delegates about changes if validation is correct to delegate (SocialFeedSelectCellDelegate). Originally, this tableView appeared in SignUpViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate, SocialFeedSelectCellDelegate only.
Problem : The same UITableView should appear in two different places (SignUpViewController and SettingsViewController). Also SignUpViewController and SettingsViewController should know about success or fail of account validation.
What I tried : I created SocialFeedTableViewController: UITableViewController, SocialFeedSelectCellDelegate for the tableView with two cells. Set view in SocialFeedTableViewController as container view for SignUpViewController and SettingsViewController. I used second delegation (from SocialFeedTVC to SignUp and Settings) to notify SignUp and Settings about validation changes. I think it is bad idea, because of double delegation. Teammate said me that it is hard to understand.
Question: What is the best and simple design solution for the problem?
Why is the double delegation a problem? As far as I see it you have 2 table views, 1 for each controller. Then each controller sets the delegate to each of the table view as self. Even if not it is quite common to change the delegate of the object in runtime. It is also normal to have 2 delegate properties with the same protocol simply to be able to forward the message to 2 objects or more.
There are many alternatives as well. You may use the default notification center and be able to forward the messages this way. The only bad thing about it is you need to explicitly resign the notification listener from the notification center.
Another more interesting procedure in your case is creating a model (a class) that holds the data from the table view and also implements the protocol from the cells. The model should then be forwarded to the new view controller as a property. If the view controller still needs to refresh beyond the table view then the model should include another protocol for the view controller itself.
Take something like this for example:
protocol ModelProtocol: NSObjectProtocol {
func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?)
}
class DelegateSystem {
class Model: NSObject, UITableViewDelegate, UITableViewDataSource, ModelProtocol {
// My custom cell class
class MyCell: UITableViewCell {
weak var modelDelegate: ModelProtocol?
var indexPath: NSIndexPath?
func onTextChanged(field: UITextField) { // just an example
modelDelegate?.cellDidUpdateText(self, text: field.text) // call the cell delegate
}
}
// some model values
var firstTextInput: String?
var secondTextInput: String?
// a delegate method from a custom protocol
func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?) {
// update the appropriate text
if cell.indexPath?.row == 0 {
self.firstTextInput = text
} else {
self.secondTextInput = text
}
}
// table view data source
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = MyCell() // create custom cell
cell.indexPath = indexPath // We want to keep track of the cell index path
// assign from appropriate text
if cell.indexPath?.row == 0 {
cell.textLabel?.text = self.firstTextInput
} else {
cell.textLabel?.text = self.secondTextInput
}
cell.modelDelegate = self // set the delegate
return cell
}
}
// The first view controller class
class FirstViewController: UIViewController {
var tableView: UITableView? // most likely from storyboard
let model = Model() // generate the new model
override func viewDidLoad() {
super.viewDidLoad()
refresh() // refresh when first loaded
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
refresh() // Refresh each time the view appears. This will include when second view controller is popped
}
func refresh() {
if let tableView = self.tableView {
tableView.delegate = model // use the model as a delegate
tableView.dataSource = model // use the model as a data source
tableView.reloadData() // refresh the view
}
}
// probably from some button or keyboard done pressed
func presentSecondController() {
let controller = SecondViewController() // create the controller
controller.model = model // assign the same model
self.navigationController?.pushViewController(controller, animated: true) // push it
}
}
// The second view controller class
class SecondViewController: UIViewController {
var tableView: UITableView? // most likely from storyboard
var model: Model? // the model assigned from the previous view controller
override func viewDidLoad() {
super.viewDidLoad()
refresh() // refresh when first loaded
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
refresh() // Refresh each time the view appears. This will include when third view controller is popped
}
func refresh() {
if let tableView = self.tableView {
tableView.delegate = model // use the model as a delegate
tableView.dataSource = model // use the model as a data source
tableView.reloadData() // refresh the view
}
}
// from back button for instance
func goBack() {
self.navigationController?.popViewControllerAnimated(true)
}
}
}
Here the 2 view controllers will communicate with the same object which also implements the table view protocols. I do not suggest you to put all of this into a single file but as you can see both of the view controllers are extremely clean and the model takes over all the heavy work. The model may have another delegate which is then used by the view controllers themselves to forward additional info. The controllers should then "steal" the delegate slot from the model when view did appear.
I hope this helps you understand the delegates are not so one-dimensional and a lot can be done with them.
Related
In interface builder, I embedded two instances of a UITableViewController in container views in a UIStackView. Both TableViewControllers are linked to the same custom class document (see code below). The only difference between them is in the data they display. Both have UITableViews that allow multiple selection – but I also want so that selecting anything in one table causes the deselection of everything in the other table, and vice versa. I tried setting this up with delegation, but I don't know how to reference one instance from the other within UITableViewController, to assign each as the delegate of the other.
I couldn't find anything relevant about delegation or about referencing a view controller by anything other than its subclass name. So in my latest attempt, I tried referring to the other child of the parent object. Here's the relevant code:
protocol TableViewSelectionDelegate: AnyObject {
func didSelectInTableView(_ tableView: UITableView)
}
class TableViewController: UITableViewController, TableViewSelectionDelegate {
weak var delegate: TableViewSelectionDelegate?
#IBOutlet var numbersTableView: UITableView!
#IBOutlet var lettersTableView: UITableView!
// Received by segue
var displayables: [Character] = []
override func viewDidLoad() {
super.viewDidLoad()
}
// (It's too soon to determine parents/children in viewDidLoad())
override func viewWillAppear(_ animated: Bool) {
guard let tableViewControllers = parent?.children else {
print("No tableViewControllers found!")
return
}
switch restorationIdentifier {
case "NumbersTableViewController":
for tableViewController in tableViewControllers {
if tableViewController.restorationIdentifier == "LettersTableViewController" {
delegate = tableViewController as? TableViewSelectionDelegate
}
}
case "LettersTableViewController":
for tableViewController in tableViewControllers {
if tableViewController.restorationIdentifier == "NumbersTableViewController" {
delegate = tableViewController as? TableViewSelectionDelegate
}
}
default: print("Unidentified Table View Controller")
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectInTableView(tableView)
}
func didSelectInTableView(_ tableView: UITableView) {
switch tableView {
case numbersTableView:
numbersTableView.indexPathsForSelectedRows?.forEach { indexPath in
numbersTableView.deselectRow(at: indexPath, animated: false)
}
case lettersTableView:
lettersTableView.indexPathsForSelectedRows?.forEach { indexPath in
lettersTableView.deselectRow(at: indexPath, animated: false)
}
default: print("Unidentified Table View")
}
}
}
Running the above and tapping in either table results in "Unidentified Table View" printed to the console, and neither table's selections are cleared by making a selection in the other.
Any insights into how I could get the results that I want would be appreciated. If something here isn't clear, let me know, and I'll make updates.
Passing information between two instances of a UITableViewController through delegation is apparently not as complicated as it at first seemed. The only noteworthy part is the setting of the delegate. Within the custom TableViewController class, when one instance is initialized, it needs to set itself as the delegate of the other instance. That's it!
In this case, to reference one instance from within another, one can use the tableViewController's parent to get to the other child tableViewController. Although there might be a better way to do this, see the code for my particular solution. Notably, since the parent property is not yet set just after viewDidLoad(), I needed to set things up in viewWillAppear(). Also note that this approach doesn't require using restorationIdentifiers or tags. Rather, it indirectly determines the tableViewController instance through its tableView property.
The delegated didSelectInTableView() function passes the selectedInTableView that was selected in the other tableViewController instance. Since the delegate needs to clear its own selected rows, the selectedInTableView is not needed for this purpose. That is, for just clearing rows, the function doesn't need to pass anything.
protocol TableViewSelectionDelegate: AnyObject {
func didSelectInTableView(_ selectedInTableView: UITableView)
}
class TableViewController: UITableViewController, TableViewSelectionDelegate {
weak var delegate: TableViewSelectionDelegate?
// Received by segue
var displayables: [Character] = []
override func viewDidLoad() {
super.viewDidLoad()
}
// (It's too soon to determine parents/children in viewDidLoad())
override func viewWillAppear(_ animated: Bool) {
guard let siblingTableViewControllers = parent?.children as? [TableViewController] else { return }
switch tableView {
case siblingTableViewControllers[0].tableView: siblingTableViewControllers[1].delegate = self
case siblingTableViewControllers[1].tableView: siblingTableViewControllers[0].delegate = self
default: print("Unidentified Table View Controller")
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectInTableView(tableView)
}
func didSelectInTableView(_ selectedInTableView: UITableView) {
// selectedTableView is NOT the one that needs to be cleared
// The function only makes it available for other purposes
tableView.indexPathsForSelectedRows?.forEach { indexPath in
tableView.deselectRow(at: indexPath, animated: false)
}
}
}
Please feel free to correct my conceptualization and terminology.
This question already has answers here:
Passing data between view controllers
(45 answers)
Closed 5 years ago.
I've got this problem, in the gif attached you can see it: if I tap on the row of UrgenzaViewController it gets back to Ho fumatoViewController, and what I need is that the Label in UITableViewCell "Urgenza" will be modified with the title of the row pressed in UrgenzaViewController. How to modify the label in the custom cell? Thanks everybody
In your Urgenza view controller create a delegate at the top of your file (above your class declaration, below the import statements) like this:
protocol UrgenzaDelegate: class {
func menuItemSelected(item: String)
}
Then inside your Urgenza class declaration create an instance of the delegate like this :
weak var delegate: UrgenzaDelegate?
Then inside didSelectRowAtIndexPath method I would call the delegate method like this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let delegate = delegate {
delegate.menuItemSelected(item: dataSource[indexPath.row])
}
}
Replace 'dataSource' with whatever data source you are using to populate the cell labels.
Finally, in your initial view controller (Ho fumatoViewController) you need to conform to the delegate you just created. You can do this by making an extension like this :
extension fumatoViewController: UrgenzaDelegate {
func menuItemSelected(item: String) {
// Here is where you save the selected item to whatever data source you are using
tableView.reloadData()
}
}
And lastly, and very important!, wherever you are pushing the Urgenza view controller you must set yourself to its delegate property like so:
let vc = UrgenzaViewController()
vc.delegate = self // This is the important part!
self.present(vc, animated: true, completion: nil)
I am trying to create a subclass of UITableView in Swift.
var tableScroll = HorizontalScrollTableView(frame: CGRectMake(15, 15, cell.contentView.frame.width - 30, cell.contentView.frame.height - 30))
cell.contentView.addSubview(tableScroll)
I add the table in the following way and then I have a HorizontalScrollTableView swift file with
import UIKit
class HorizontalScrollTableView : UITableView {
func numberOfSectionsInTableView(tableView: UITableView) -> Int
{
return 1
}
...
However the Table presents like a normal default table and the functions in HorizontalScrollTableView do not override the default ones.
You're probably looking to override numberOfSections instead of numberOfSectionsInTableView:, which is a method on the UITableViewDataSource protocol. However, I believe it's more wise to create your own subclass of UITableViewController or your class conforming to UITableViewDataSource and set it as your table view's delegate rather than of the table view itself. Subclassing UIView and its descendants is usually reserved for customizing view-specific functionality and attributes, such as adding custom drawing or custom touch handling.
I went ahead and made a simple example using a Playground. You can see table view isn't subclassed, but instead there is a view controller which serves as the table view's delegate and a stand-in data source for the table view.
The data source provides the row data for the table view and the delegate (which is also the view controller) provides the cells.
This is a skeleton for how I normally set up my table views, although I would use an XIB generally.
import UIKit
// data for table view is array of String
let rowData = [ "Chicago", "Milwaukee", "Detroit", "Lansing" ]
//
// simple subclass of UITableViewCell that has an associated ReuseIdentifier
// and a value property. Setting the value property changes what the cell displays
//
public class TableViewCell : UITableViewCell
{
public static let ReuseIdentifier = "TableViewCell"
var value:AnyObject? {
didSet {
self.textLabel!.text = value as? String
}
}
}
//
// Simple implementation of a table view data source--just contains one String per row
// You could change your rowData to be an array of Dictionary for richer content possibilities.
// You could also load your data from a JSON file... for example
//
class TableViewDataSource : NSObject, UITableViewDataSource
{
var rowData:[String]?
init( rowData:[String] )
{
self.rowData = rowData
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return rowData?.count ?? 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
assert( indexPath.section == 0 ) // we only have 1 section, so if someone section ≠ 0, it's a bug
var cell:TableViewCell! = tableView.dequeueReusableCellWithIdentifier( "Cell" ) as? TableViewCell
if cell == nil
{
cell = TableViewCell( style: .Default, reuseIdentifier: "Cell" )
}
cell.value = self.rowData![ indexPath.row ]
return cell
}
}
// This is the view controller for our table view.
// The view controller's view happens to be a table view, but your table view
// could actually be a subview of your view controller's view
class TableViewController : UIViewController, UITableViewDelegate
{
// data source for our table view, lazily created
lazy var tableViewDataSource:TableViewDataSource = TableViewDataSource( rowData: rowData )
override func loadView()
{
// our view is a UITableView
let tableView = UITableView()
tableView.delegate = self
tableView.dataSource = self.tableViewDataSource // using a self-contained data source object for this example
self.view = tableView
}
}
let window:UIWindow! = UIWindow()
window.rootViewController = TableViewController()
window.makeKeyAndVisible() // click the "preview eyeball" to see the window
I have two different classes that both implement the UITableViewDataSource and UITableViewDelegate protocols. They are separate from my UITableViewController.
I would like to choose the correct data source class to instantiate in viewDidLoad() and then set UITableViewController to be a delegate of UITableViewDataSource and UITableViewDelegate classes. (I return an object from these classes to UITableViewController for prepareForSegue to know what to display in the detail view controller screen.)
This doesn't work.
At runtime it breaks without a runtime error, just with "Thread 1: EXC_BAD_ACCESS (code=1, address=...) the line "class AppDelegate: UIResponder, UIApplicationDelegate {"
However, if I define the data source object as an instance variable in the UITableViewConroller (as opposed to doing it within viewDidLoad()) then it works. Of course, this defeats the purpose, since now I can't switch to another data source.
It seems that if I want to set UITableViewController as a delegate (i.e., want to be able to send data back from data source) then I can't do this in viewDidLoad() for some reason. Maybe it hasn't finished creating the objects yet? (Everything works if I create objects as instance variables and immediately initialise them.)
protocol GroupByDelegator {
func callSegueFromGroupByDelegator()
}
class RemindersViewController: UITableViewController, GroupByDelegator {
#IBOutlet var remindersTableview: UITableView!
// var dataSource = GroupByNothingDataSource() // THIS WORKS, BUT THEN I CAN'T CHANGE THE DATASOURCE ANYMORE
var reminderWrapperToBeDisplayedInDetailView: ReminderWrapper?
override func viewDidLoad() {
super.viewDidLoad()
// if ... {
var dataSource = GroupByNothingDataSource() // BREAKS THE CODE
// } else {
// var dataSource = GroupByPriorityDataSource()
// }
dataSource.groupByDelegator = self // used so that the datasource can call the callSegueFromGroupByDelegator() func that will pass an object back to here.
self.tableView.dataSource = dataSource
self.tableView.delegate = dataSource
}
...
// This function is called by the data source delegates (GroupByNothingDataSource, GroupByPriorityDataSource) because they can't perform segues.
func callSegueFromGroupByDelegator(reminderWrapper: ReminderWrapper?) {
reminderWrapperToBeDisplayedInDetailView = reminderWrapper
//try not to send self, just to avoid retain cycles
self.performSegueWithIdentifier("reminderDetail", sender: tableView)
}
}
class GroupByPriorityDataSource: NSObject, UITableViewDataSource, UITableViewDelegate, TableViewCellDelegate, RemindersViewControllerDelegate {
var groupByDelegator: GroupByDelegator!
...
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
...
// Pass object back to UITableViewController
self.groupByDelegator.callSegueFromGroupByDelegator(reminderWrapper?)
}
}
I ended up initializing both datasources as class instance variables (instead of initializing only one dynamically depending on the user interaction.) Now I choose between the already initialized datasources dynamically depending on the user interaction.
It solved my problem, but hasn't really addressed the technical issue. Is this a bug in the platform?
I have created a TableView application following the "Beginning iPhone Development with Swift " book.The search Bar tableView is created with code and not within the storyboard.The book explains how to get search results and display the corresponding cells but I would like my app to perform a segue to a ViewController I have created in the storyBoard.How can I trigger a Segue with code ?
for more info , this is my file :
import UIKit
class SearchResultsController: UITableViewController , UISearchResultsUpdating{
let sectionsTableIdentifier = "section identifier"
var products = [product]()
var filteredProducts = [product]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerClass(UITableViewCell.self,
forCellReuseIdentifier: sectionsTableIdentifier)
}
// MARK: - Table view data source
override func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return filteredProducts.count
}
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(
sectionsTableIdentifier) as UITableViewCell
cell.textLabel!.text = filteredProducts[indexPath.row].name
return cell }
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "detailView"{
let index = self.tableView?.indexPathForSelectedRow()
var destinationViewController : infoViewController = segue.destinationViewController as infoViewController
destinationViewController.Title = filteredProducts[index!.row].title
destinationViewController.eam = filteredProducts[index!.row].energy
destinationViewController.fam = filteredProducts[index!.row].fat
destinationViewController.pam = filteredProducts[index!.row].protein
destinationViewController.cam = filteredProducts[index!.row].carbohydrates
destinationViewController.imgName = filteredProducts[index!.row].imgName
}
}
func updateSearchResultsForSearchController(
searchController: UISearchController) {
let searchString = searchController.searchBar.text
filteredProducts.removeAll()
for prod in products{
var name = prod.name.lowercaseString
if name.rangeOfString(searchString) != nil {
filteredProducts.append(prod)
}
}
tableView.reloadData()
}}
Because the controller is built in code, you need to use the SearchResultsController's tableView delegate method didSelectRowAtIndexPath to trigger the presentation of the next view controller.
Assuming that there is a table view controller underpinning the SearchResultsController, you could potentially use that as the delegate of the SearchResultsController. The main table view controller might already have the necessary code to segue when a cell is selected, in which case you need to check which tableView has been selected in order to correctly determine which product the cell represents.
To set the delegate, add the following line to the code (in your comment above) where you create the SearchResultsController:
resultsController.tableView?.delegate = self
Then amend the didSelectRowAtIndexPath method to test which tableView is triggering the method:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if (tableView == self.tableView) {
// use the existing code to present the detail VC, based on the data in the main table view
...
} else {
// use new code to present the detail VC, based on data from the SearchResultsController
...
}
}
If the main table view controller is in a storyboard, you can use a segue to present the detail VC. In this case you would use self.performSegueWithIdentifier() in the above code. If not, you would either use self.navigationController?.pushViewController() (if you are embedded in a navigation controller) or self.presentViewController() (to present the detail VC modally).
Another option would be to set the SearchResultsController's delegate to be self (in viewDidLoad), and then to implement didSelectRowAtIndexPath in the SearchResultsController class. In this case, you don't need to test which tableView has triggered the method, but you will not be able to use a segue.