I have a tab bar controller application and in one of the tabs a UI Collection View Controller with an action assigned to a button. This button does its magic and then should change the tab bar view to another one. However, I'm not able to reference it right to the tab controller.
tabBarController is the class name assigned to the controller. So, I tried:
tabBarController.selectedIndex = 3
and also creating a method directly in the tabBarController class
tabBarController.goToIndex(3)
The error says: Instance member of 'goToIndex' cannot be used on type tabBarController
Any ideia?
Thank you,
Im having a little trouble understanding what you mean by referencing it right, but hopefully this will help. Assuming tabBarController is a subclass of UITabBarController:
class MyTabBarController: UITabBarController {
/// ...
func goToIndex(index: Int) {
}
}
In one of your tab controllers (UIViewController) you can reference your UITabBarController with self.tabBarController. Note that self.tabBarController is optional.
self.tabBarController?.selectedIndex = 3
If your tab UIViewController is a UIViewController inside a UINavigationController, then you will need to reference your tab bar like this:
self.navigationController?.tabBarController
To call a function on your subclass you would need to cast the tab bar controller to your custom subclass.
if let myTabBarController = self.tabBarController as? MyTabBarController {
myTabBarController.goToIndex(3)
}
Update based on comments:
You're correct that you cant access the tabBarController inside the cell unless you made it a property on either the cell itself (not recommended) or the app delegate. Alternatively, you could use target action on your UIViewController to call a function on the view controller every time a button is tapped inside a cell.
class CustomCell: UITableViewCell {
#IBOutlet weak var myButton: UIButton!
}
class MyTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReuseIdentifier", for: indexPath) as! CustomCell
/// Add the indexpath or other data as a tag that we
/// might need later on.
cell.myButton.tag = indexPath.row
/// Add A Target so that we can call `changeIndex(sender:)` every time a user tapps on the
/// button inside a cell.
cell.myButton.addTarget(self,
action: #selector(MyTableViewController.changeIndex(sender:)),
for: .touchUpInside)
return cell
}
/// This will be called every time `myButton` is tapped on any tableViewCell. If you need
/// to know which cell was tapped, it was passed in via the tag property.
///
/// - Parameter sender: UIButton on a UITableViewCell subclass.
func changeIndex(sender: UIButton) {
/// now tag is the indexpath row if you need it.
let tag = sender.tag
self.tabBarController?.selectedIndex = 3
}
}
Related
I created button in my custom cell which is I created to another class after that I added that cell on my tableView(in my viewController), when the user click the that button which is in custom cell, I have to segue another class and also I have to pass some data but problem is I could not segue in custom cell, so how can I detect when user tap that button ? or how can I take current index of that button which user tapped ?
First you have to make one protocol on your custom table cell.
Then create one weak var in your table cell class.
After that make your button action in your custom table cell class and pass button tag from your table cell to view controller. From the button tag you can easily get which index button is clicked.
protocol CustomTableCellDelegate: AnyObject {
func btnClicked(tag: Int)
}
class CustomTableCell: UITableViewCell {
..
..
weak var customTableCellDelegate: CustomTableCellDelegate?
..
..
#IBAction func btnClicked(_ sender: UIButton) {
self.customTableCellDelegate?.btnClicked(tag: sender.tag)
}
Now in your viewcontroller (where your table cell is implementing and showing list in tableview) make one extension and give delegate to that extension.
extension YourViewController: CustomTableCellDelegate {
func btnClicked(tag: Int) {
// Here you can find the which button is clicked from the button tag. You can do segue or navigation from here.
}
}
Make sure to do this. This is important
In your cellForRowAt method, write two below lines.
cell.customTableCellDelegate = self
cell.yourbutton.tag = indexPath.row
I'm working on an iOS app in Xcode/Swift and I'm trying to add a subview UISendViewButton in my MainViewController when a UITableViewCell button is clicked in a separate UITableViewController (which itself is embedded in a UIView). Basically, the concept is that of a "send post" button like in Instagram: the user will click a paper airplane button and a separate list of friends appears (UIView -> UITableViewController). Next to the list of contacts, there is a button (customButton) that the user can click to choose which friends to send it to. What I want is to have a "Send" button (UIViewButton) appear ONLY if the user decides to click the button (customButton) next to their friends' name.
I was able to make the UITableViewController appear by embedding it within a UIView and then adding that as a subview to my MainViewController, but when I click the customButton in the UITableViewCell class, nothing happens. I would like for a new UIViewButton (in my MainViewController) to appear when I click the customButton.
So basically I wanted to know how to have these two controllers communicate. The controller which houses the UITableViewCell button is the one that is provided by Xcode's library: UITableViewController -> UITableView -> UITableViewCell which is itself embedded within a UIView.
I tried using a delegate on the UITableViewCell class below:
import UIKit
public protocol CustomCellDelegate: class {
func customButtonClick()
}
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var customButton: UIButton!
weak var delegate: CustomCellDelegate?
#IBAction func customButtonAction(_ sender: UIButton) {
delegate?.customButtonClick()
}
.. }
And corresponding code on the MainViewController here:
import UIKit
class MainViewController: UIViewController, CustomCellDelegate {
#IBOutlet var UIViewButton: UIView!
func customButtonClick() {
self.UIViewButton.frame = CGRect (x:20, y: 300, width: 369, height: 46)
self.view.addSubview(UIViewButton)
} }
I know that CustomTableViewCell is within two different controllers (CustomTableView, CustomTableViewController), so I was thinking there may be a delegate issue within one of those/I may need to add another delegate for those controllers but I'm not sure how. I've managed to change the customButton icon in CustomTableViewCell so I know that it's clickable, I just can't seem to get it to delegate or communicate with the MainViewController and have the UIViewButton appear. I'm sorry for the confusion and inconvenience, any help with this would be greatly appreciated as I'm a beginner to coding. Thanks so much!! ^__^
UPDATE:
CustomTableViewController:
public protocol ContactListTableViewControllerDelegate: class {
func showSendButton() }
class CustomTableViewController: UITableViewController, CustomCellDelegate {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "likesCell", for: indexPath) as! CustomTableViewCell
cell.delegate = self }
func customButtonClick() {
delegate?.showSendButton()
} }
MainViewController:
extension MainViewController: ContactListTableViewControllerDelegate {
func showSendButton() {
self.UISendButton.frame = CGRect (x:20, y: 300, width: 369, height: 46)
self.view.addSubview(UISendButton)
} }
I hope that your UI is similar to the image below.
For this, I would do the following
Declare a protocol that will let you know when to show or hide the 'Send' button.
For example
protocol ContactListTableViewControllerDelegate: class {
func showSendButton()
func hideSendButton()
}
Make the first view controller the delegate for the table view controller class
You have already created a protocol for picking up tap events on the button inside the cell. Optionally you need to add the cell too as a parameter, so that the delegate knows which cell was clicked.
public protocol CustomCellDelegate: class {
func customButtonClick(cell: CustomTableViewCell)
}
When creating each cell in cellForRowAtIndexPath method, make the table view controller the delegate for the cell
cell.delegate = self
Implement the delegate method in the table view controller subclass
func customButtonClick(cell: CustomTableViewCell) {
// Find the index path for the clicked cell
// You should have an array for storing the indices of selected cells
// Check if the index is already in the array. If yes, remove it from
//the array (deselection)
// If no, add the index to the array(selection)
// Check the count of the array
// If it is > 0, it means at least one cell is selected. Call the
// delegate method
// to show button
delegate?.showSendButton()
// Else call delegate method to hide send button
delegate?.hideSendButton()
}
So I have tableview embedded in collectionview.
I have xib for tableview.
When user select a cell of tableview I want to navigate to another view controller.
I tried this method but its not working
let storyboardId = "Login"
let vc = storyboard?.instantiateViewController(withIdentifier: storyboardId)
navigationController?.pushViewController(vc!, animated: true)
But its not working because this viewcontroller in not added to navigation stack.
class DetailCollectionViewCell: UICollectionViewCell, UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// let navigationController = UINavigationController()
// let storyboardId = "Login"
// let vc = storyboard?.instantiateViewController(withIdentifier: storyboardId)
// navigationController?.pushViewController(vc!, animated: true)
}
}
How do i solve this problem.
Any help is appreciated.
You have following options
1) Implement tableview datasource and delgate in viewController instead of collection view cell
2) Use Delegate (explained below )
3) Use Closures
4) Use NotificationCenter
You need to create delegate or protocol as collection view cell can't push or present view controller.
Here is simple example (This is not exact code you may need modification)
Create protocol
protocol TableViewInsideCollectionViewDelegate:class {
func cellTaped(data:IndexPath)
}
Inside your collectionview cell add weak property
weak var delegate:TableViewInsideCollectionViewDelegate?
Now in your ViewController class you in cellForItem method of collectionview
you need to set delegate to self
like
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YourCell", for: indexPath) as! CustomCollectionCell
cell.delegate = self
return cell
and implement delegate method in viewController class and write code to push your view controller from there like self.navigationController.push
Now In Goto Collectionview Cell method
and whenever your tableviewDidSelect called
call delegate method like self.delegate?.cellTaped(data: dataYouWantToPass)
Hope it is helpful
You have to check some info:
First:
Check your navigationController is nil or not
Second:
Check your initial view controller method is correct or not,
this is my way:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "StoryboardIdentifier") as? ViewController
You can solve this problem by using a delegation pattern use the following steps :
Confirm table view delegate to the collection view and collection view delegate to the respective view controller.
delegation chaining can be used to solve this problem. In this example, I have shown how you can pass data from table view cell to collection view cell.
Implement collection view delegate and data source methods.
Implement table view delegate and data source methods.
whenever did select row will get called the call delegate method to tell view controller that some row is selected and according to the row index change handle your navigation.
code example:
Step 1: Create a protocol.
protocol RowSelected : class {
func rowSelected(_ index : Int)
}
Step 2: Declare delegate variable in TableViewCell.
weak var delegate: RowSelected?
Step 3: In collection view confirm delegate and Implement delegated method.
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellIdentifier", for: indexPath) as! CollectionViewCell
cell.delegate = self
return cell
extension CollectionViewCell : RowSelected {
func rowSelected() {
// Pass the information to view controller via a delegate/closure/notification. just like we passed information from table view cell to collection view cell and handle navigation accordingly.
}
}
Step 4: In ViewController you can confirm a delegate for collection view and implement it's delegate method and can handle navigation.
You can use a closure and notification center as well to inform view controller to navigate to next screen.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let storyboard = UIStoryboard(name: "FilteringPageSelection", bundle: nil)
controller = storyboard.instantiateViewControllerWithIdentifier("FilteringPageSelectionController") as! FilteringPageSelectionTableViewController
controller.filteringType = filterTitle[indexPath.row];
controller.selectedValue = selectedValue;
controller.title = "Selection"
self.navigationController?.pushViewController(controller, animated: true)
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
Above is my code, this is my table view controller code, i am intend to push to another view controller by clicking it cell. Inside the new controller, there is multiple value for me to choose. however , after i select the new value from the new controller, i back to the original table view scene and get the selected value from the new controller and assign into the variable selectedValue. Below is my code of the unwind segue.
#IBAction func unwindToFilteringVC(segue:UIStoryboardSegue) {
selectedValue = controller.selectedValue
}
However, when i click the cell again, since the tableview override function only initialize for the first time when the tableview is loaded, so when i change the value of the selectedValue, the controller.selectedValue still remain the old value and push into new controller.
How do i changes the value of controller.selectedValue?
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) - this function gets called every time you tap on a cell, not just once. So whenever you select a cell, your code instantiates the filter view controller and sets its selectedValue to the current selectedValue of the table view controller (unless that is a global variable). Maybe that is not what you want.
OR
You may have mixed which selectedValue is which. You should rename one.
I think what you need to use is a delegate pattern to inform the table view controller of the selected value in the selection view controller instead of holding a reference to it. First you define a protocol:
protocol SelectionViewControllerDelegate: class {
func selectionViewController(controller: SelectionViewController, didSelectValue value: ValueType)
}
You add a weak reference to an optional property that conforms to that protocol and you inform the delegate of changes in the selected value right where you handle the selection action in your selection view controller.
class SelectionViewController: UIViewController {
weak var delegate: SelectionViewControllerDelegate?
func handleChange() {
delegate?.selectionViewController(self, didSelectValue: selectedValue)
}
}
Then you make your table view controller conform to this protocol like so:
class TableViewController: UITableViewController, SelectionViewControllerDelegate {
func selectionViewController(controller: SelectionViewController, didSelectValue value: ValueType) {
self.selectedValue = value
}
}
Don't forget to set the table view controller as the delegate of the selection view controller in the didSelectRowAtIndexPath method by saying:
controller.delegate = self
Right before you call navigationController?.pushViewController method.
I have two files, MyTableViewController and myViewController. I set UIImageView on TableCell in MyTableVIewController. myViewController does not contain anything. And I created an array called ImageArray which contains an array of Images.
What I am aiming to do here is when I click an image on TableCell in myTableViewController, I want the clicked image to appear in the myViewController. And some description about the clicked image beside the image too. I want to use myViewController for users to get detailed information of the selected image.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
var ImageView = cell.viewWithTag(1) as! UIImageView
ImageView.image = UIImage(named: ImageArray[indexPath.row])
ImageView.contentMode = .ScaleAspectFit
var TextView = cell.viewWithTag(2) as! UILabel
TextView.text = ImageArray[indexPath.row]
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
performSegueWithIdentifier("next", sender: indexPath)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "next") {
let destination = segue.destinationViewController as! myViewController
}
}
I don't know what to do to make it happen. I really appreciate if you could help me figure out! Thanks!
First and foremost, I'm assuming your MyTableViewController class conforms to both UITableViewDataSource and UITableViewDelegate protocols and that you've set your MyTableViewController class to be the delegate in code or via Storyboard.
With that sorted out,there are multiple ways to achieve the result you seek.
You can declare your ImageArray in an independent class and call it inside your MyTableViewController class, index them onto a tableView using the tableView delegate methods, and finally using the prepareForSegue method to push your images onto your myViewController. Or you can simply declare and initialize your ImageArray at the top of your MyTableViewController class like below:
var ImageArray = [("Moscow Russia.jpg", "Europe"),
("London England.jpg", "Europe")]
In the ImageArray above, ensure that your image name matches exactly as the asset name you've imported into your Xcode project.
Then we specify how many rows in section we need ImageArray to occupy on our tableView (i.e. basically count our ImageArray into our TableView) with below required method:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return ImageArray.count ?? 0
}
Next, you want to present your ImageArray in each row of of the cell using the tableView:cellForRowAtIndexPath: method.
Side Note on your TableCell: Hopefully your TableCell is subclassed from UITableViewCell and you have already declared and connected two IBOutlets, say, imageView and textLabel respectively. Also, ensure your TableCell is properly linked to your prototype cell in Storyboard under Identity Inspector) Your TableCell class should look something like below:
import UIKit
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var textLabel: UILabel!
}
Now back into your MyTableVIewController class. From your code, I see you're casting the line 'let cell = ...' as 'UITableViewCell. You should rather cast it as 'TableCell' instead since you're subclassing it. Implement the tableView:cellForRowAtIndexPath: method as follows:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell") as! TableCell
//Note that I'm using tuples here. Pretty cool huh. Got to love Swift!
let (imageName, textNameToGoWithImage) = ImageArray[indexPath.row]
cell.textLabel.text = textNameToGoWithImage
cell.imageView.image = UIImage(named: imageName)
cell.imageView.contentMode = .ScaleAspectFit
// You could have also used 'if let' in optional binding to safely unwrap your image if you want like below.
// if let image = UIImage(named: imageName){
// cell.imageView?.image = image
// cell.imageView.contentMode = .ScaleAspectFit
// }
return cell
}
It looks like you're a little confused about when and where to use performSegueWithIdentifier method as opposed to using -prepareForSegue method. And even when to use the tableView:didSelectRowAtIndexPath method.
Let me briefly explain here. You use the performSegueWithIdentifier method when you didn't control-drag a segue from one ViewController's scene to another in Storyboard. This way, using the performSegueWithIdentifier method will allow you to move between ViewController scenes as long as you specify the right identifier which you've set in Storyboard under 'Attributes Inspector.'
Now if you're using Storyboard instead, you wouldn't need the tableView:didSelectRowAtIndexPath method. What the tableView:didSelectRowAtIndexPath method does is that it tells the delegate that the specified row is now selected and we can do something within its code body (like push an image or a text onto another ViewController Scene like you're trying to do). But that becomes redundant when you use segues. All you have to do, is to control-drag a segue from the table cell on your MyTableViewController scene to your myViewController scene. Choose 'Show' and give the segue an identifier name like you've done "next". (A little side note: if you want the Back button functionality to display at top navigator bar when you run your app, you simply embed your MyTableViewController in a UINavigationController to give you that 'Back' button functionality. With your MyTableViewController Scene selected in Storyboard, Go to the top menu and select Editor >> Embed In >> Navigation Controller. Then walla!!)
Lets now go ahead and implement our tableView:prepareForSegue method like below:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "next" {
//Note that, originally, destinationViewController is of Type UIViewController and has to be casted as myViewController instead since that's the ViewController we trying to go to.
let destinationVC = segue.destinationViewController as! myViewController
if let indexPath = self.tableView.indexPathForSelectedRow{
let selectedRow = ImageArray[indexPath.row]
destinationVC.imageName2 = selectedRow.0
destinationVC.textName2 = selectedRow.1
}
From the above code, make sure you set the 'imageName' and 'textName' as properties in your myViewController class first before you can access them with 'destinationVC' which is now of type myViewController. These two properties will hold the data we are passing from MyTableViewController class to myViewController class. And we are using the array index to pass data to these two properties accordingly.
You can then create two IBOutlets to display your image and text by passing these set 'imageName2' and 'textName2' properties to your outlets (or any UI control for that matter).
Now the reason why you will have to set properties first in
myViewController class before you pass them on or around (i.e. to a
UI element, closure, another VC etc) is that, when you hit a tableView cell from
MyTableViewController scene to segue onto your next ViewController
scene (i.e. your myViewController scene), iOS hasn't instantiated
that second scene just yet. And so you need a property to hold onto the data
you're trying to pass onto your second scene View Controller first so that you can
make use of it later when that class finally loads.
So your myViewController class should look something like below:
import UIKit
class myViewController : UIViewController {
//Your two strings to initially hold onto the data
//being passed to myViewController class
var imageName2 : String?
var textName2 : String?
#IBOutlet weak var detailImageView: UIImageView!
#IBOutlet weak var detailTextNameLabel: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
detailTextNameLabel.text = textName2!
if let image = UIImage(named: imageName2!) {
self.detailImageView.image = image
}
}
And that's it!
Things to note on labelling and conventions in Swift:
Start naming classes with block letters (i.e. class
ViewController() {})
Classes and properties should capture the meaning of what they
represent. I will recommend you change your MyTableViewController
and 'myViewController'classes accordingly to reflect what they truly
mean or do (You can go with 'MainTableViewController' and 'DetailViewController'. That will do just fine).
Use camelToe labelling for properties and methods. (I used the labels you
provided in your question in order not to confuse you too much).
Enjoy!
This should help out:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "next") {
let destination = segue.destinationViewController as! myViewController
destination.imageView.image = UIImage(named: ImageArray[tableView.indexPathForSelectedRow?.row])
destination.textView.text = ImageArray[tableView.indexPathForSelectedRow?.row]
}
}
(Where imageView and textView are views in your new viewController.)
Note:
tableView.indexPathForSelectedRow?.row should give you the selected row, as the name implies, but it can be nil, so be careful.
In addition, Swift variable naming conventions are camelCase, so imageView is the correct way, while ImageView is incorrect.
In swift 3 you can do something like this:
In your MyTableViewController class:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! UITableViewCell
// for imageview
var ImageView : UIImageView = cell.contentView.viewWithTag(1) as! UIImageView
ImageView.image = UIImage(named: ImageArray[indexPath.row])
// for text
var TextView : UILabel = cell.contentView.viewWithTag(2) as! UILabel
ImageView.text = ImageArray[indexPath.row]
return cell
}
And in your didSelectRow method:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let nextVC = self.storyboard?.instantiateViewController(withIdentifier: "myViewController") as! myViewController
nextVC.myImgView.image = UIImame(named: ImageArray[indexPath.row])
nextVC.myLabel.text = ImageArray[indexPath.row]
self.present(nextVC!, animated: true)
}
And inmyViewController class:
class myViewController: UIViewController
{
let myImageView = UIImageView()
let myLabel = UILabel()
}